@samitouri / QOSAMI-WSL / commits / 358ab87d

Add WSLC (WSL Containers) feature (#40366)

WSLC is a container runtime built on the Windows Subsystem for Linux, enabling Windows applications to create and manage Linux containers through a native Windows API surface. Key components: - wslc.exe: CLI for managing containers, images, volumes, and networks (build, run, stop, inspect, push/pull from registries) - wslcsession.exe: Per-user Windows service hosting container lifecycle, storage management, and networking - WSLC SDK: C++ and C# client libraries with NuGet packaging for programmatic container management - Container networking: port forwarding, DNS tunneling, virtio networking, and HCN integration - Storage: VHD-backed volumes, virtiofs file sharing, overlayfs layers - GPU passthrough and device host proxy support Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: 1wizkid <richard.fricks@hotmail.com> Co-authored-by: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Co-authored-by: beena352 <beenachauhan@microsoft.com> Co-authored-by: Blue <OneBlue@users.noreply.github.com> Co-authored-by: Craig Loewen <crloewen@microsoft.com> Co-authored-by: Darshak Bhatti <47045043+dabhattimsft@users.noreply.github.com> Co-authored-by: David Bennett <dbenne@microsoft.com> Co-authored-by: Feng Wang <wang6922@outlook.com> Co-authored-by: Flor Chacon <14323496+florelis@users.noreply.github.com> Co-authored-by: John Stephens <johnstep@microsoft.com> Co-authored-by: JohnMcPMS <johnmcp@microsoft.com> Co-authored-by: Kevin Vega <40717198+kvega005@users.noreply.github.com> Co-authored-by: Pooja Trivedi <poojatrivedi@gmail.com> Co-authored-by: ramesh-ramn <raman.ramesh@gmail.com> Co-authored-by: Richard Fricks <richfr@microsoft.com> Co-authored-by: yao-msft <50888816+yao-msft@users.noreply.github.com>

Ben Hillis committed Apr 30, 2026 at 13:34 UTC 358ab87d409104af47f72b6410c30ff92cff54d9
384 files changed +73620 -2751
.github/CODEOWNERS
+2 -1
@@ -1,4 +1,5 @@
1 # File containing policy for file ownership
2
3 # Reviewers for all files in the repository
4 -* @microsoft/wsl-maintainers
\ No newline at end of file
4 +* @microsoft/wsl-maintainers
5 +* @microsoft/wslc-team
\ No newline at end of file
.gitignore
+4 -1
@@ -68,4 +68,7 @@ tools/hooks/pre-commit
68 doc/site/
69 directory.build.targets
70 test-storage/
71 -*.vhdx
\ No newline at end of file
71 +*.vhdx
72 +*.tar
73 +*.etl
74 +*.lscache
.pipelines/build-job.yml
+76 -1
@@ -125,7 +125,7 @@ jobs:
125 displayName: "CMake ${{ parameters.platform }}"
126 inputs:
127 workingDirectory: "."
128 - cmakeArgs: . --fresh -A ${{ parameters.platform }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_VERSION=10.0.26100.0 -DPACKAGE_VERSION=$(version.WSL_PACKAGE_VERSION) -DWSL_NUGET_PACKAGE_VERSION=$(version.WSL_NUGET_PACKAGE_VERSION) -DSKIP_PACKAGE_SIGNING=${{ parameters.isRelease }} -DOFFICIAL_BUILD=${{ parameters.isRelease }} -DINCLUDE_PACKAGE_STAGE=${{ or(parameters.isRelease, parameters.isNightly) }} -DPIPELINE_BUILD_ID=$(Build.BuildId) -DVSO_ORG=${{ parameters.vsoOrg }} -DVSO_PROJECT=${{ parameters.vsoProject }} -DWSL_BUILD_WSL_SETTINGS=true $(packageInputDirArg)\${{ parameters.platform }}
128 + cmakeArgs: . --fresh -A ${{ parameters.platform }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_VERSION=10.0.26100.0 -DPACKAGE_VERSION=$(version.WSL_PACKAGE_VERSION) -DWSL_NUGET_PACKAGE_VERSION=$(version.WSL_NUGET_PACKAGE_VERSION) -DSKIP_PACKAGE_SIGNING=${{ parameters.isRelease }} -DOFFICIAL_BUILD=${{ parameters.isRelease }} -DINCLUDE_PACKAGE_STAGE=${{ or(parameters.isRelease, parameters.isNightly) }} -DPIPELINE_BUILD_ID=$(Build.BuildId) -DVSO_ORG=${{ parameters.vsoOrg }} -DVSO_PROJECT=${{ parameters.vsoProject }} -DWSL_BUILD_WSL_SETTINGS=true -DWSL_BUILD_SDKCS=true $(packageInputDirArg)\${{ parameters.platform }}
129
130 # Workaround for WSL Settings NuGet restore authentication issue
131 - script: _deps\nuget.exe restore -NonInteractive
@@ -210,6 +210,77 @@ jobs:
210 Copy-Item -Path "bin\${{ parameters.platform }}\release\wsl.msi" -Destination "$(ob_outputDirectory)\bundle\wsl.$(version.WSL_PACKAGE_VERSION).${{ parameters.platform }}.msi"
211 Copy-Item -Path "bin\${{ parameters.platform }}\release\installer.msix" -Destination "$(ob_outputDirectory)\installer\installer.${{ parameters.platform }}.msix"
212 if (Test-Path "generated\dev-cert.pfx") { Copy-Item "generated\dev-cert.pfx" "$(ob_outputDirectory)\installer\dev-cert.pfx" }
213 + New-Item -ItemType Directory -Path "$(ob_outputDirectory)\sdk\${{ parameters.platform }}" -Force
214 + Copy-Item -Path "bin\${{ parameters.platform }}\release\wslcsdk.lib" -Destination "$(ob_outputDirectory)\sdk\${{ parameters.platform }}\wslcsdk.lib"
215 + Copy-Item -Path "bin\${{ parameters.platform }}\release\wslcsdk.dll" -Destination "$(ob_outputDirectory)\sdk\${{ parameters.platform }}\wslcsdk.dll"
216 + Copy-Item -Path "bin\${{ parameters.platform }}\release\wslcsdkcs.dll" -Destination "$(ob_outputDirectory)\sdk\${{ parameters.platform }}\wslcsdkcs.dll"
217 +
218 + - task: PowerShell@2
219 + displayName: "Create CAB from ${{ parameters.platform }} installer msi"
220 + inputs:
221 + targetType: inline
222 + script: |
223 + $arch = '${{ parameters.platform }}'
224 + $bundleDir = "$(ob_outputDirectory)\bundle"
225 + $msiPath = Join-Path $bundleDir "wsl.$(version.WSL_PACKAGE_VERSION).$arch.msi"
226 + $cabPath = Join-Path $bundleDir "wsl.$(version.WSL_PACKAGE_VERSION).$arch.cab"
227 +
228 + if (-not (Test-Path -Path $msiPath)) {
229 + throw "Input MSI not found for architecture '$arch': $msiPath"
230 + }
231 +
232 + Write-Host "Creating CAB from MSI: $msiPath -> $cabPath"
233 + & makecab.exe $msiPath $cabPath
234 +
235 + if ($LASTEXITCODE -ne 0) {
236 + throw "makecab.exe failed with exit code $LASTEXITCODE for architecture '$arch'."
237 + }
238 +
239 + if (-not (Test-Path -Path $cabPath)) {
240 + throw "CAB file was not created at expected path: $cabPath"
241 + }
242 +
243 + - ${{ if eq(parameters.isRelease, true) }}:
244 + - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5
245 + displayName: "Sign CAB (${{ parameters.platform }})"
246 + inputs:
247 + ConnectedServiceName: ${{ parameters.esrp.ConnectedServiceName}}
248 + signConfigType: ${{ parameters.esrp.signConfigType }}
249 + SessionTimeout: ${{ parameters.esrp.SessionTimeout }}
250 + MaxConcurrency: ${{ parameters.esrp.MaxConcurrency }}
251 + MaxRetryAttempts: ${{ parameters.esrp.MaxRetryAttempts }}
252 + ServiceEndpointUrl: ${{ parameters.esrp.ServiceEndpointUrl }}
253 + AuthAKVName: ${{ parameters.esrp.AuthAKVName }}
254 + AuthSignCertName: ${{ parameters.esrp.AuthSignCertName }}
255 + AppRegistrationClientId: ${{ parameters.esrp.AppRegistrationClientId }}
256 + AppRegistrationTenantId: ${{ parameters.esrp.AppRegistrationTenantId }}
257 + FolderPath: "$(ob_outputDirectory)\\bundle"
258 + Pattern: "*.cab"
259 + UseMSIAuthentication: true
260 + EsrpClientId: ${{ parameters.esrp.EsrpClientId }}
261 + inlineOperation: |
262 + [
263 + {
264 + "KeyCode": "CP-230012",
265 + "OperationCode": "SigntoolSign",
266 + "Parameters" : {
267 + "OpusName" : "Microsoft",
268 + "OpusInfo" : "http://www.microsoft.com",
269 + "FileDigest" : "/fd \"SHA256\"",
270 + "PageHash" : "/NPH",
271 + "TimeStamp" : "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
272 + },
273 + "ToolName" : "sign",
274 + "ToolVersion" : "1.0"
275 + },
276 + {
277 + "KeyCode" : "CP-230012",
278 + "OperationCode" : "SigntoolVerify",
279 + "Parameters" : {},
280 + "ToolName" : "sign",
281 + "ToolVersion" : "1.0"
282 + }
283 + ]
284
285 - powershell: |
286 $binFolder = ".\bin\${{ parameters.platform }}\Release"
@@ -230,6 +301,7 @@ jobs:
301
302 Copy-Item -Path "bin\x64\release\wsltests.dll" -Destination "$(ob_outputDirectory)\testbin\x64\release\wsltests.dll"
303 Copy-Item -Path "bin\x64\release\testplugin.dll" -Destination "$(ob_outputDirectory)\testbin\x64\release\testplugin.dll"
304 + Copy-Item -Path "bin\x64\release\wslcsdk.dll" -Destination "$(ob_outputDirectory)\testbin\x64\release\wslcsdk.dll"
305 Copy-Item -Path "bin\x64\release\installer.msix" -Destination "$(ob_outputDirectory)\testbin\x64\release\installer.msix"
306 Move-Item -Path "packages\Microsoft.Taef.$taefVersion\build\Binaries\x64" -Destination "$(ob_outputDirectory)\testbin\x64\release\taef"
307
@@ -243,6 +315,9 @@ jobs:
315 $TestDistroVersion = (Select-Xml -Path packages.config -XPath '/packages/package[@id=''Microsoft.WSL.TestDistro'']/@version').Node.Value
316 Copy-Item "packages\Microsoft.WSL.TestDistro.$TestDistroVersion\x64\test_distro.tar.xz" "$(ob_outputDirectory)\testbin\x64"
317
318 + $TestDataVersion = (Select-Xml -Path packages.config -XPath '/packages/package[@id=''Microsoft.WSL.TestData'']/@version').Node.Value
319 + Copy-Item "packages\Microsoft.WSL.TestData.$TestDataVersion\x64" "$(ob_outputDirectory)\testbin\x64\test_data" -Recurse -Force
320 +
321 displayName: Stage test artifacts
322
323 - task: PublishSymbols@2
.pipelines/build-stage.yml
+7 -2
@@ -7,6 +7,10 @@ parameters:
7 type: string
8 default: ""
9
10 + - name: nugetSuffix
11 + type: string
12 + default: ""
13 +
14 - name: isNightly
15 type: boolean
16 default: false
@@ -19,6 +23,7 @@ parameters:
23 type: object
24 default:
25 - Microsoft.WSL.PluginApi.nuspec
26 + - Microsoft.WSL.Containers.nuspec
27
28 - name: includePackageStage
29 type: boolean
@@ -27,8 +32,8 @@ parameters:
32 - name: targets
33 type: object
34 default:
30 - - target: "wsl;libwsl;wslg;wslservice;wslhost;wslrelay;wslinstaller;wslinstall;initramfs;wslserviceproxystub;wslsettings;wslinstallerproxystub;testplugin"
31 - 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,WSLDVCPlugin.dll,testplugin.dll,wsldeps.dll"
35 + - target: "wsl;libwsl;wslg;wslservice;wslhost;wslrelay;wslinstaller;wslinstall;initramfs;wslserviceproxystub;wslsettings;wslinstallerproxystub;testplugin;wslcsession;wslc;wsltests;wslcsdk;wslcsdkcs"
36 + 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,WSLDVCPlugin.dll,testplugin.dll,wsldeps.dll,wslcsession.exe,wslc.exe,wslcsdk.dll,wslcsdkcs.dll"
37 - target: "msixgluepackage"
38 pattern: "gluepackage.msix"
39 - target: "msipackage"
.pipelines/nuget-stage.yml
+2 -1
@@ -7,6 +7,7 @@ parameters:
7 type: object
8 default:
9 - Microsoft.WSL.PluginApi
10 + - Microsoft.WSL.Containers
11
12 stages:
13 - stage: nuget
@@ -39,7 +40,7 @@ stages:
40 # Note: this task might fail if there's been no commits between two nightly pipelines, which is fine.
41 - ${{ each package in parameters.nugetPackages }}:
42 - task: NuGetCommand@2
42 - displayName: Push nuget/${{ package }}.$(WSL_NUGET_PACKAGE_VERSION).nupkg
43 + displayName: Push nuget/${{ package }}.*.nupkg
44 inputs:
45 command: 'push'
46 packagesToPush: $(Build.SourcesDirectory)\drop\nuget\${{ package }}.$(WSL_NUGET_PACKAGE_VERSION).nupkg
.pipelines/package-stage.yml
+3
@@ -81,6 +81,9 @@ stages:
81 $dest = "bin\$($arch.dir)\Release"
82 New-Item -ItemType Directory -Path $dest -Force
83 Copy-Item "$(Pipeline.Workspace)\drop_$($arch.platform)\installer\installer.$($arch.platform).msix" "$dest\installer.msix"
84 + Copy-Item "$(Pipeline.Workspace)\drop_$($arch.platform)\sdk\$($arch.platform)\wslcsdk.lib" "$dest\wslcsdk.lib"
85 + Copy-Item "$(Pipeline.Workspace)\drop_$($arch.platform)\sdk\$($arch.platform)\wslcsdk.dll" "$dest\wslcsdk.dll"
86 + Copy-Item "$(Pipeline.Workspace)\drop_$($arch.platform)\sdk\$($arch.platform)\wslcsdkcs.dll" "$dest\wslcsdkcs.dll"
87 }
88
89 # Copy MSIs to the output bundle directory
.pipelines/test-job.yml
+1 -2
@@ -46,5 +46,4 @@ jobs:
46 cancelTimeoutInMinutes: 420
47 TestTimeout: "0.05:00:00"
48 parserProperties: "worker:VsTestVersion=V150;session:HoldTrigger=Failure;VstsTestResultAttachmentUploadBehavior=Always"
49 - notificationSubscribers: $(Build.RequestedForEmail)
50 - scheduleBuildRequesterAlias: "lowdev"
49 + notificationSubscribers: $(Build.RequestedForEmail)
\ No newline at end of file
.pipelines/test-stage.yml
+1
@@ -16,6 +16,7 @@ parameters:
16 default:
17 - wsl1
18 - wsl2
19 + - wslc
20
21 - name: test_images
22 type: object
.pipelines/wsl-build-nightly-onebranch.yml
+5 -1
@@ -49,4 +49,8 @@ extends:
49
50 - template: test-stage.yml@self
51 parameters:
52 - rs_prerelease_only: false
\ No newline at end of file
52 + rs_prerelease_only: false
53 +
54 + - template: nuget-stage.yml@self
55 + parameters:
56 + isNightly: true
\ No newline at end of file
.pipelines/wsl-build-release-onebranch.yml
+7 -1
@@ -8,6 +8,11 @@ parameters:
8 displayName: 'Test the release pipeline'
9 type: string
10 default: ''
11 +
12 +- name: nugetSuffix
13 + displayName: 'Nuget version suffix (must include "-")'
14 + type: string
15 + default: ''
16
17 trigger:
18 tags:
@@ -55,6 +60,7 @@ extends:
60 parameters:
61 isRelease: true
62 packageVersion: ${{ iif(eq(parameters.testVersion, ''), variables['Build.SourceBranchName'], parameters.testVersion) }}
63 + nugetSuffix: ${{ parameters.nugetSuffix }}
64 traceLoggingConfig: $(ReleaseTraceLoggingConfig)
65 vsoOrg: microsoft
66 vsoProject: Microsoft.WSL
@@ -69,7 +75,7 @@ extends:
75 packageVersion: ${{ iif(eq(parameters.testVersion, ''), variables['Build.SourceBranchName'], parameters.testVersion) }}
76 bypassTests: ${{ parameters.bypassTests }}
77
72 - - ${{ if eq(parameters.testVersion, '') }}:
78 + - ${{ if or(eq(parameters.testVersion, ''), not(eq(parameters.nugetSuffix, ''))) }}:
79 - template: nuget-stage.yml@self
80 parameters:
81 isNightly: false
\ No newline at end of file
CMakeLists.txt
+66 -8
@@ -80,6 +80,31 @@ FetchContent_Declare(nlohmannjson
80 FetchContent_MakeAvailable(nlohmannjson)
81 FetchContent_GetProperties(nlohmannjson SOURCE_DIR NLOHMAN_JSON_SOURCE_DIR)
82
83 +FetchContent_Declare(yaml-cpp
84 + URL https://github.com/jbeder/yaml-cpp/releases/download/yaml-cpp-0.9.0/yaml-cpp-yaml-cpp-0.9.0.tar.gz
85 + URL_HASH SHA256=298593d9c440fd9034b8b193d96318b76d49bc97c6ceadb7b0836edf0b6d7539)
86 +
87 +set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
88 +set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
89 +set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE)
90 +set(YAML_MSVC_SHARED_RT OFF CACHE BOOL "" FORCE)
91 +set(BUILD_TESTING OFF CACHE BOOL "" FORCE) # Prevents yaml-cpp from generating CTest* targets
92 +
93 +FetchContent_MakeAvailable(yaml-cpp)
94 +
95 +set(BOOST_VERSION "1.90.0")
96 +set(BOOST_TARBALL "boost_${BOOST_VERSION}")
97 +string(REPLACE "." "_" BOOST_TARBALL "${BOOST_TARBALL}")
98 +
99 +FetchContent_Declare(
100 + boost_headers
101 + URL https://archives.boost.io/release/${BOOST_VERSION}/source/${BOOST_TARBALL}.tar.gz
102 + URL_HASH SHA256=5e93d582aff26868d581a52ae78c7d8edf3f3064742c6e77901a1f18a437eea9
103 +)
104 +
105 +FetchContent_MakeAvailable(boost_headers)
106 +
107 +
108 # Import modules
109 list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake")
110 find_package(IDL REQUIRED)
@@ -88,6 +113,7 @@ find_package(NUGET REQUIRED)
113 find_package(VERSION REQUIRED)
114 find_package(MC REQUIRED)
115 find_package(Appx REQUIRED)
116 +find_package(CSharp REQUIRED)
117
118 # Download nuget packages
119 restore_nuget_packages()
@@ -105,6 +131,7 @@ find_nuget_package(Microsoft.WSL.Kernel KERNEL /build/native)
131 find_nuget_package(Microsoft.WSL.bsdtar BSDTARD /build/native/bin)
132 find_nuget_package(Microsoft.WSL.LinuxSdk LINUXSDK /)
133 find_nuget_package(Microsoft.WSL.TestDistro TEST_DISTRO /)
134 +find_nuget_package(Microsoft.WSL.TestData WSL_TEST_DATA /)
135 find_nuget_package(Microsoft.WSLg WSLG /build/native/bin)
136 find_nuget_package(vswhere VSWHERE /tools)
137 find_nuget_package(Wix WIX /tools/net6.0/any)
@@ -145,6 +172,10 @@ if (NOT DEFINED WSL_BUILD_WSL_SETTINGS)
172 set(WSL_BUILD_WSL_SETTINGS false)
173 endif ()
174
175 +if (NOT DEFINED WSL_BUILD_SDKCS)
176 + set(WSL_BUILD_SDKCS false)
177 +endif ()
178 +
179 find_commit_hash(COMMIT_HASH)
180
181 if (NOT PACKAGE_VERSION)
@@ -164,9 +195,21 @@ if (OFFICIAL_BUILD AND NOT PACKAGE_VERSION MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9
195 message(FATAL_ERROR "PACKAGE_VERSION is invalid: '${PACKAGE_VERSION}'. Needs to match '([0-9]+).([0-9]+).([0-9]+).0' for official builds")
196 endif()
197
167 -# Configure per-config output directories
198 +if (${TARGET_PLATFORM} STREQUAL "x64")
199 + set(DCAT_PRODUCT_NAME WSL.amd64)
200 +endif()
201 +
202 +if (${TARGET_PLATFORM} STREQUAL "arm64")
203 + set(DCAT_PRODUCT_NAME WSL.arm64)
204 +endif()
205 +
206 +set(DCAT_REGISTRATION_KEY "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Update\\TargetingInfo\\DynamicInstalled\\${DCAT_PRODUCT_NAME}")
207 +
208 +# Configure output directories
209 +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${TARGET_PLATFORM})
210 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Debug)
211 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Release)
212 +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
213
214 set_property(GLOBAL PROPERTY USE_FOLDERS ON)
215
@@ -243,7 +286,9 @@ add_compile_definitions(UNICODE
286 WSL_PACKAGE_VERSION_MINOR=${PACKAGE_VERSION_MINOR}
287 WSL_PACKAGE_VERSION_REVISION=${PACKAGE_VERSION_REVISION}
288 DISTRO_HOSTTYPE="${DISTRO_HOSTTYPE}"
246 - WSL_BUILD_WSL_SETTINGS=${WSL_BUILD_WSL_SETTINGS})
289 + WSL_BUILD_WSL_SETTINGS=${WSL_BUILD_WSL_SETTINGS}
290 + DCAT_PRODUCT_NAME=R"\(${DCAT_PRODUCT_NAME}\)"
291 + DCAT_REGISTRATION_KEY=R"\(${DCAT_REGISTRATION_KEY}\)")
292
293 if (${OFFICIAL_BUILD})
294 add_compile_definitions(WSL_OFFICIAL_BUILD)
@@ -270,6 +315,12 @@ if (${TARGET_PLATFORM} STREQUAL "x64")
315 set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /CETCOMPAT")
316 endif()
317
318 +if (WSL_BUILD_SDKCS OR WSL_BUILD_WSL_SETTINGS)
319 + set(CMAKE_CSharp_FLAGS "${CMAKE_CSharp_FLAGS} /langversion:latest /debug:full")
320 + set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk")
321 + set(CMAKE_DOTNET_TARGET_FRAMEWORK "net8.0-windows${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}")
322 +endif()
323 +
324 # Common link libraries
325 link_directories(${WSLDEPS_SOURCE_DIR}/lib/)
326 set(COMMON_LINK_LIBRARIES
@@ -283,6 +334,7 @@ set(COMMON_LINK_LIBRARIES
334 Shlwapi.lib
335 synchronization.lib
336 Bcrypt.lib
337 + Crypt32.lib
338 icu.lib)
339
340 set(MSI_LINK_LIBRARIES
@@ -334,6 +386,10 @@ if (NOT VS_INSTALL_DIR)
386 message(FATAL_ERROR "Could not determine Visual Studio installation directory.")
387 endif()
388
389 +# Pick the first line returned from vswhere, in case multiple versions are returned.
390 +# TODO: Remove this and require VS 2026 once the CI supports it.
391 +string(REGEX MATCH "([^\n\r]+)" VS_INSTALL_DIR "${VS_INSTALL_DIR}")
392 +
393 if("${CMAKE_HOST_SYSTEM_PROCESSOR}" STREQUAL "AMD64")
394 set(LLVM_INSTALL_DIR "${VS_INSTALL_DIR}/VC/Tools/Llvm/x64/bin")
395 else()
@@ -354,12 +410,6 @@ set(WSL_PRE_COMMIT_MODE "warn" CACHE STRING "Pre-commit hook behavior: warn, err
410 file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/tools/hooks)
411 configure_file(${CMAKE_CURRENT_LIST_DIR}/tools/hooks/pre-commit.in ${CMAKE_BINARY_DIR}/tools/hooks/pre-commit @ONLY)
412
357 -cmake_path(COMPARE "${wsl_SOURCE_DIR}" EQUAL "${wsl_BINARY_DIR}" BUILD_IN_SOURCE)
358 -if (NOT ${BUILD_IN_SOURCE}) # Testing on 3.26 project_type_DIR paths appear canonicalized
359 - file(CREATE_LINK ${LLVM_INSTALL_DIR}/clang-format.exe ${wsl_SOURCE_DIR}/tools/clang-format.exe COPY_ON_ERROR)
360 - file(COPY_FILE ${CMAKE_BINARY_DIR}/tools/hooks/pre-commit ${wsl_SOURCE_DIR}/tools/hooks/pre-commit ONLY_IF_DIFFERENT)
361 -endif()
362 -
413 set(LINUXSDK_PATH ${LINUXSDK_SOURCE_DIR}/${LLVM_ARCH})
414 set(LLVM_TARGET "${LLVM_ARCH}-unknown-linux-musl")
415 set(LINUX_CC ${LLVM_INSTALL_DIR}/clang.exe)
@@ -446,6 +496,7 @@ endif()
496
497 # Common include paths
498 include_directories(${CMAKE_CURRENT_SOURCE_DIR}/wil/include)
499 +include_directories(${boost_headers_SOURCE_DIR})
500 include_directories(${WSLDEPS_SOURCE_DIR}/include)
501 include_directories(${WSLDEPS_SOURCE_DIR}/include/Windows)
502 include_directories(${WSLDEPS_SOURCE_DIR}/include/schemas)
@@ -478,6 +529,7 @@ add_subdirectory(msipackage)
529 add_subdirectory(msixinstaller)
530 add_subdirectory(src/windows/common)
531 add_subdirectory(src/windows/service)
532 +add_subdirectory(src/windows/wslcsession)
533 add_subdirectory(src/windows/wslinstaller/inc)
534 add_subdirectory(src/windows/wslinstaller/stub)
535 add_subdirectory(src/windows/wslinstaller/exe)
@@ -487,6 +539,12 @@ add_subdirectory(src/windows/wslg)
539 add_subdirectory(src/windows/wslhost)
540 add_subdirectory(src/windows/wslrelay)
541 add_subdirectory(src/windows/wslinstall)
542 +add_subdirectory(src/windows/wslc)
543 +add_subdirectory(src/windows/WslcSDK)
544 +
545 +if (WSL_BUILD_SDKCS)
546 + add_subdirectory(src/windows/WslcSDK/csharp)
547 +endif()
548
549 if (WSL_BUILD_WSL_SETTINGS)
550 add_subdirectory(src/windows/libwsl)
UserConfig.cmake.sample
+5 -2
@@ -33,8 +33,11 @@ if(WSL_DEV_BINARY_PATH)
33 endforeach()
34 endif()
35
36 -# # Uncomment to skip building, packaging and installing wslsettings
37 -# set(WSL_BUILD_WSL_SETTINGS false)
36 +# # Uncomment to build, package and install wslsettings
37 +# set(WSL_BUILD_WSL_SETTINGS true)
38 +
39 +# # Uncomment to build the C# WSLC sdk
40 +# set(WSL_BUILD_SDKCS true)
41
42 # # Uncomment to generate a "thin" MSI package which builds and installs faster
43 # set(WSL_BUILD_THIN_PACKAGE true)
cgmanifest.json
+12
@@ -51,6 +51,18 @@
51 }
52 }
53 },
54 + {
55 + "component": {
56 + "type": "other",
57 + "other": {
58 + "name": "yaml-cpp",
59 + "version": "0.9.0",
60 + "downloadUrl": "https://github.com/jbeder/yaml-cpp/releases/download/yaml-cpp-0.9.0/yaml-cpp-yaml-cpp-0.9.0.tar.gz",
61 + "hash": "sha256:298593d9c440fd9034b8b193d96318b76d49bc97c6ceadb7b0836edf0b6d7539"
62 +
63 + }
64 + }
65 + },
66 {
67 "component": {
68 "type": "other",
cloudtest/CMakeLists.txt
+11
@@ -45,7 +45,18 @@ function(add_test_group image version)
45 configure_file(${CMAKE_CURRENT_SOURCE_DIR}/TestGroup.xml.in ${DIR}/TestGroup.xml)
46 endfunction()
47
48 +function(add_wslc_test_group image)
49 + set(version "c")
50 + set(DIR ${OUT}/${image}-wslc)
51 +
52 + file(MAKE_DIRECTORY ${DIR})
53 +
54 + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/TestMap.xml.in ${DIR}/TestMap.xml)
55 + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/TestGroup-wslc.xml.in ${DIR}/TestGroup.xml)
56 +endfunction()
57 +
58 foreach(image ${CLOUDTEST_IMAGES})
59 add_test_group("${image}" "1")
60 add_test_group("${image}" "2")
61 + add_wslc_test_group("${image}")
62 endforeach()
cloudtest/TestGroup-wslc.xml.in new
+27
@@ -0,0 +1,27 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<TestJobGroup EnableProcessJobObjectBreakaway="true">
3 + <ResourceSpec>
4 + <Resource SKU="Standard_D4_v3" Image="${image}"/>
5 + </ResourceSpec>
6 + <Setup TimeoutMins="30">
7 + <BuildFiles>
8 + <Copy Src="${TEST_PACKAGE_PROVIDER}\${TEST_PACKAGE_PATH}" Dest="[WorkingDirectory]" IsRecursive="false"/>
9 + <Copy Src="[drop]\testbin\${TARGET_PLATFORM}\release\*" Dest="[WorkingDirectory]\" IsRecursive="true" Writable="true"/>
10 + <Copy Src="[drop]\testbin\${TARGET_PLATFORM}\test_distro.tar.xz" Dest="[WorkingDirectory]" IsRecursive="false" Writable="true"/>
11 + <Copy Src="[drop]\testbin\${TARGET_PLATFORM}\test_data\*" Dest="[WorkingDirectory]\test_data" IsRecursive="true" Writable="true"/>
12 + <Copy Src="[drop]\testbin\test-setup.ps1" Dest="[WorkingDirectory]\" IsRecursive="false" />
13 + <Copy Src="[drop]\testbin\CloudTest-Setup.bat" Dest="[WorkingDirectory]\" IsRecursive="false" />
14 + <Copy Src="[drop]\testbin\wsl.wprp" Dest="[WorkingDirectory]\" IsRecursive="false" />
15 + <Copy Src="[drop]\testbin\unit_tests\*" Dest="[WorkingDirectory]\unit_tests" IsRecursive="true" Writable="true"/>
16 + <Copy Src="[test_packages]\*" Dest="[WorkingDirectory]" IsRecursive="false" />
17 + <Copy Src="[dump_tool]\DumpTool.exe" Dest="[WorkingDirectory]" IsRecursive="false" />
18 + </BuildFiles>
19 + <Scripts>
20 + <Script Path="[WorkingDirectory]\CloudTest-Setup.bat" Args="[WorkingDirectory] [LoggingDirectory]" />
21 + </Scripts>
22 + </Setup>
23 +
24 + <TestJob Name="CloudTest.Taef" TimeoutMins="120">
25 + <Execution Type="TAEF" Path="[WorkingDirectory]\wsltests.dll" Args="/p:bugReportDirectory=[LoggingDirectory]\BugReportOutput /errorOnCrash /testmode:etwlogger /EtwLogger:WPRProfileFile=[WorkingDirectory]\wsl.wprp /EtwLogger:WPRProfile=WSL /EtwLogger:SavePoint=ExecutionComplete /EtwLogger:RecordingScope=Execution /p:SetupScript=.\test-setup.ps1 /p:Package=[WorkingDirectory]\${TEST_PACKAGE_FILE} /p:Version=2 /p:AllowUnsigned=${ALLOW_UNSIGNED_PACKAGE} /p:UnitTestsPath=[WorkingDirectory]\unit_tests /p:DistroPath=[WorkingDirectory]\test_distro.tar.xz /p:TestDataPath=[WorkingDirectory]\test_data /p:DistroName=test_distro /logOutput:High /p:RedirectStdout=[LoggingDirectory]\stdout.txt /p:RedirectStderr=[LoggingDirectory]\stderr.txt /p:KernelLogs=[LoggingDirectory]\dmesg.txt /p:DumpFolder=[LoggingDirectory] /p:WerReport /p:LogDmesg /p:PipelineBuildId=${PIPELINE_BUILD_ID} /p:DumpTool=DumpTool.exe /select:&quot;@TestCategory='WSLC' and (@WSLVersion='2' or not(@WSLVersion='*'))&quot;" />
26 + </TestJob>
27 +</TestJobGroup>
cloudtest/TestGroup.xml.in
+2 -1
@@ -8,6 +8,7 @@
8 <Copy Src="${TEST_PACKAGE_PROVIDER}\${TEST_PACKAGE_PATH}" Dest="[WorkingDirectory]" IsRecursive="false"/>
9 <Copy Src="[drop]\testbin\${TARGET_PLATFORM}\release\*" Dest="[WorkingDirectory]\" IsRecursive="true" Writable="true"/>
10 <Copy Src="[drop]\testbin\${TARGET_PLATFORM}\test_distro.tar.xz" Dest="[WorkingDirectory]" IsRecursive="false" Writable="true"/>
11 + <Copy Src="[drop]\testbin\${TARGET_PLATFORM}\test_data\*" Dest="[WorkingDirectory]\test_data" IsRecursive="true" Writable="true"/>
12 <Copy Src="[drop]\testbin\test-setup.ps1" Dest="[WorkingDirectory]\" IsRecursive="false" />
13 <Copy Src="[drop]\testbin\CloudTest-Setup.bat" Dest="[WorkingDirectory]\" IsRecursive="false" />
14 <Copy Src="[drop]\testbin\wsl.wprp" Dest="[WorkingDirectory]\" IsRecursive="false" />
@@ -21,6 +22,6 @@
22 </Setup>
23
24 <TestJob Name="CloudTest.Taef" TimeoutMins="240">
24 - <Execution Type="TAEF" Path="[WorkingDirectory]\wsltests.dll" Args="/p:bugReportDirectory=[LoggingDirectory]\BugReportOutput /errorOnCrash /testmode:etwlogger /EtwLogger:WPRProfileFile=[WorkingDirectory]\wsl.wprp /EtwLogger:WPRProfile=WSL /EtwLogger:SavePoint=ExecutionComplete /EtwLogger:RecordingScope=Execution /p:SetupScript=.\test-setup.ps1 /p:Package=[WorkingDirectory]\${TEST_PACKAGE_FILE} /p:Version=${version} /p:AllowUnsigned=${ALLOW_UNSIGNED_PACKAGE} /p:UnitTestsPath=[WorkingDirectory]\unit_tests /p:DistroPath=[WorkingDirectory]\test_distro.tar.xz /p:DistroName=test_distro /logOutput:High /p:RedirectStdout=[LoggingDirectory]\stdout.txt /p:RedirectStderr=[LoggingDirectory]\stderr.txt /p:KernelLogs=[LoggingDirectory]\dmesg.txt /p:DumpFolder=[LoggingDirectory] /p:WerReport /p:LogDmesg /p:PipelineBuildId=${PIPELINE_BUILD_ID} /p:DumpTool=DumpTool.exe /select:&quot;not(@TestCategory='WSLC') and (@WSLVersion='${version}' or not(@WSLVersion='*'))&quot;" />
25 + <Execution Type="TAEF" Path="[WorkingDirectory]\wsltests.dll" Args="/p:bugReportDirectory=[LoggingDirectory]\BugReportOutput /errorOnCrash /testmode:etwlogger /EtwLogger:WPRProfileFile=[WorkingDirectory]\wsl.wprp /EtwLogger:WPRProfile=WSL /EtwLogger:SavePoint=ExecutionComplete /EtwLogger:RecordingScope=Execution /p:SetupScript=.\test-setup.ps1 /p:Package=[WorkingDirectory]\${TEST_PACKAGE_FILE} /p:Version=${version} /p:AllowUnsigned=${ALLOW_UNSIGNED_PACKAGE} /p:UnitTestsPath=[WorkingDirectory]\unit_tests /p:DistroPath=[WorkingDirectory]\test_distro.tar.xz /p:TestDataPath=[WorkingDirectory]\test_data /p:DistroName=test_distro /logOutput:High /p:RedirectStdout=[LoggingDirectory]\stdout.txt /p:RedirectStderr=[LoggingDirectory]\stderr.txt /p:KernelLogs=[LoggingDirectory]\dmesg.txt /p:DumpFolder=[LoggingDirectory] /p:WerReport /p:LogDmesg /p:PipelineBuildId=${PIPELINE_BUILD_ID} /p:DumpTool=DumpTool.exe /select:&quot;not(@TestCategory='WSLC') and (@WSLVersion='${version}' or not(@WSLVersion='*'))&quot;" />
26 </TestJob>
27 </TestJobGroup>
cmake/FindCSharp.cmake new
+13
@@ -0,0 +1,13 @@
1 +function(configure_csharp_target TARGET)
2 + set(TARGET_PLATFORM_MIN_VERSION "10.0.19041.0")
3 +
4 + set_target_properties(
5 + ${TARGET} PROPERTIES
6 + VS_GLOBAL_TargetPlatformVersion "${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}"
7 + VS_GLOBAL_TargetPlatformMinVersion "${TARGET_PLATFORM_MIN_VERSION}"
8 + VS_GLOBAL_WindowsSdkPackageVersion "${WINDOWS_SDK_DOTNET_VERSION}"
9 + VS_GLOBAL_AppendRuntimeIdentifierToOutputPath false
10 + VS_GLOBAL_GenerateAssemblyInfo false
11 + VS_GLOBAL_TargetLatestRuntimePatch false
12 + )
13 +endfunction()
\ No newline at end of file
cmake/FindIDL.cmake
+26 -9
@@ -11,6 +11,12 @@ function(add_idl target idl_files_with_proxy idl_files_no_proxy)
11 endforeach()
12
13 string(TOLOWER ${TARGET_PLATFORM} IDL_ENV)
14 + set(PREVIOUS_OUTPUT "")
15 +
16 + set(IDL_DLLDATA ${OUTPUT_DIR}/dlldata_${TARGET_PLATFORM}.c)
17 +
18 + list(LENGTH idl_files_with_proxy PROXY_IDL_COUNT)
19 + set(PROXY_IDL_INDEX 0)
20
21 foreach(idl_file ${idl_files_with_proxy})
22
@@ -23,19 +29,26 @@ function(add_idl target idl_files_with_proxy idl_files_no_proxy)
29 # "fatal error LNK1112: module machine type 'x64' conflicts with target machine type 'ARM64'"
30 set(IDL_I ${OUTPUT_DIR}/${IDL_NAME}_i_${TARGET_PLATFORM}.c)
31 set(IDL_P ${OUTPUT_DIR}/${IDL_NAME}_p_${TARGET_PLATFORM}.c)
26 - set(IDL_DLLDATA ${OUTPUT_DIR}/dlldata_${TARGET_PLATFORM}.c)
27 - set(MIDL_OUTPUT ${IDL_HEADER} ${IDL_I} ${IDL_P} ${IDL_DLLDATA})
32 +
33 + # Only list dlldata as a tracked output of the last MIDL command.
34 + math(EXPR PROXY_IDL_INDEX "${PROXY_IDL_INDEX} + 1")
35 + if(PROXY_IDL_INDEX EQUAL PROXY_IDL_COUNT)
36 + set(MIDL_OUTPUT ${IDL_HEADER} ${IDL_I} ${IDL_P} ${IDL_DLLDATA})
37 + else()
38 + set(MIDL_OUTPUT ${IDL_HEADER} ${IDL_I} ${IDL_P})
39 + endif()
40
41 add_custom_command(
30 - OUTPUT ${MIDL_OUTPUT} ${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/${target}
42 + OUTPUT ${MIDL_OUTPUT}
43 COMMAND midl /nologo /target NT100 /env "${IDL_ENV}" /Zp8 /char unsigned /ms_ext /c_ext /h ${IDL_HEADER} /iid ${IDL_I} /proxy ${IDL_P} /dlldata ${IDL_DLLDATA} ${idl_file} ${IDL_DEFINITIONS}
32 - COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/${target}"
44 WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
34 - DEPENDS ${idl_file}
45 + DEPENDS ${idl_file} ${PREVIOUS_OUTPUT}
46 MAIN_DEPENDENCY ${idl_file}
47 VERBATIM
48 )
49
50 + set(PREVIOUS_OUTPUT ${IDL_HEADER})
51 +
52 set_source_files_properties(${MIDL_OUTPUT} PROPERTIES GENERATED TRUE)
53 list(APPEND TARGET_OUTPUTS ${MIDL_OUTPUT})
54
@@ -48,9 +61,8 @@ function(add_idl target idl_files_with_proxy idl_files_no_proxy)
61 set(IDL_HEADER ${OUTPUT_DIR}/${IDL_NAME}.h)
62
63 add_custom_command(
51 - OUTPUT ${IDL_HEADER} ${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/${target}
52 - COMMAND midl /nologo /target NT100 /env "${IDL_ENV}" /Zp8 /char unsigned /ms_ext /c_ext /h ${IDL_HEADER} ${idl_file} ${IDL_DEFINITIONS}
53 - COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/${target}"
64 + OUTPUT ${IDL_HEADER}
65 + COMMAND midl /nologo /target NT100 /env "${IDL_ENV}" /Zp8 /char unsigned /ms_ext /c_ext /h ${IDL_HEADER} /iid nul /proxy nul /dlldata nul ${idl_file} ${IDL_DEFINITIONS}
66 WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
67 DEPENDS ${idl_file}
68 MAIN_DEPENDENCY ${idl_file}
@@ -62,6 +74,11 @@ function(add_idl target idl_files_with_proxy idl_files_no_proxy)
74
75 endforeach()
76
65 - add_custom_target(${target} DEPENDS ${TARGET_OUTPUTS} SOURCES ${idl_files_with_proxy} ${idl_files_no_proxy})
77 + # Touch the stamp file so Visual Studio's incremental build can track the
78 + # target as up-to-date.
79 + add_custom_target(${target}
80 + COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${target}
81 + DEPENDS ${TARGET_OUTPUTS}
82 + SOURCES ${idl_files_with_proxy} ${idl_files_no_proxy})
83
84 endfunction()
\ No newline at end of file
diagnostics/collect-wsl-logs.ps1
+1 -1
@@ -349,7 +349,7 @@ if ($Dump)
349 $dumpFolder = Join-Path (Resolve-Path "$folder") dumps
350 New-Item -ItemType "directory" -Path "$dumpFolder"
351
352 - $executables = "wsl", "wslservice", "wslhost", "msrdc", "dllhost"
352 + $executables = "wsl", "wslservice", "wslhost", "wslcsession", "wslrelay", "wslg", "msrdc", "dllhost"
353 foreach($process in Get-Process | Where-Object { $executables -contains $_.ProcessName})
354 {
355 $dumpFile = "$dumpFolder/$($process.ProcessName).$($process.Id).dmp"
diagnostics/wsl.wprp
+2 -2
@@ -260,7 +260,7 @@
260 <EventProvider Id="wlidsvc_WPP" Name="3F8B9EF5-BBD2-4C81-B6C9-DA3CDB72D3C5" />
261 <EventProvider Id="wsl_devicehost" Name="9d6c7b9e-2581-4d8a-b8c5-b90b4a17094a"/>
262 <EventProvider Id="wslapi" Name="beb94edf-1a7b-5058-0696-ff9e6b1798d1"/>
263 - <EventProvider Id="wslaservice" Name="0383CE62-8F86-4766-AFB2-9D66A7FB1E90"/>
263 + <EventProvider Id="wslc" Name="0383CE62-8F86-4766-AFB2-9D66A7FB1E90"/>
264 <EventProvider Id="wslclient" Name="8cbb7724-7223-5d6f-8137-564dac45104d"/>
265
266 <!-- PROFILE: General WSL Tracing (Default, also serves as base for other profiles) -->
@@ -322,7 +322,7 @@
322 <EventProviderId Value="vmwp"/>
323 <EventProviderId Value="wsl_devicehost"/>
324 <EventProviderId Value="wslapi"/>
325 - <EventProviderId Value="wslaservice"/>
325 + <EventProviderId Value="wslc"/>
326 <EventProviderId Value="wslclient"/>
327 </EventProviders>
328 </EventCollectorId>
localization/strings/cs-CZ/Resources.resw
+568
@@ -1950,4 +1950,572 @@ K dalším možnostem VS Code Remote můžete přistupovat také prostřednictv
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Integrace Visual Studia</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc – WSL Container CLI
1955 +Využití:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Relace nebyla nalezena: '{}'</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Neplatná IP adresa '{}'</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName('{}') selhalo</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>Nebyly nalezeny žádné relace WSLC.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>Nalezeno {} relací WSLC{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Ukončení relace se nezdařilo: '{}'</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} byl ukončen s: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>ID</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>PID autora</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Zobrazovaný název</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Neznámý příkaz: '{}'</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Nerozpoznaný příkaz: '{}'</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Nebyl zadán požadovaný argument: '{}'</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Argument byl zadán víckrát, než je povoleno: '{}'</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Zobrazí nápovědu k vybranému příkazu</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Bylo poskytnuto několik vzájemně se vylučujících argumentů: '{}'</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>Argument {} se dá použít jenom s {}.</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Využití: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>příkaz</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>Možnosti</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>K dispozici jsou následující aliasy příkazů:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>K dispozici jsou následující příkazy:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>K dispozici jsou následující dílčí příkazy:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>K dispozici jsou následující možnosti:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>K dispozici jsou následující argumenty:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Pokud chcete získat podrobnosti o konkrétním příkazu, předejte ho do argumentu help.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Nebyl rozpoznán název argumentu pro aktuální příkaz: '{}'</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>K provedení tohoto příkazu je třeba mít oprávnění správce.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Zadejte relaci, která se má použít</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Připojení ke stdout/stderr kontejneru</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Připojit ke stdin a ponechat jej otevřený</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Zadejte port, který se má použít</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>ID kontejneru</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Chybí hodnota argumentu: '{}'</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Nebyl rozpoznán alias argumentu pro aktuální příkaz: '{}'</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Neplatný specifikátor argumentu: '{}'</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Nenašel se sousedící alias typu příznak: '{}'</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>Sousedící alias není příznak: '{}'</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Neplatný specifikátor argumentu: '{}'</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>Argument příznaku nemůže obsahovat sousedící hodnotu: '{}'</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Našel se poziční argument, i když nebyl očekáván: '{}'</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Chybí název argumentu v: '{}'</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Nepodařilo se vyřešit přesměrované argumenty počínaje argumentem: '{}'</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Byl zjištěn neplatný nadbytečný argument: '{}'</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Argumenty aliasu s hodnotou musí být poslední v řetězci aliasů: '{}'</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Copyright (c) Microsoft Corporation. Všechna práva vyhrazena.
2122 +Informace o ochraně osobních údajů k tomuto produktu najdete na stránce https://aka.ms/privacy.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Neplatný obrázek: '{}'</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Neplatný název: '{}'</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Cesta není absolutní: '{}'</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Svazek '{}' nebyl nalezen</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Neplatné možnosti svazku: '{}'</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Nepodporovaný typ svazku: '{}'</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>Svazek '{}' je používán.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Našly se oba soubory Dockerfile i Containerfile. Použijte -f k výběru souboru, který chcete použít</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Nepodařilo se otevřít '{}': {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>V '{}' nebyl nalezen žádný Containerfile ani Dockerfile</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/da-DK/Resources.resw
+568
@@ -1950,4 +1950,572 @@ Du kan også få adgang til flere Fjernindstillinger for VS-kode via kommandopal
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Visual Studio-integration</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc - WSL Container CLI
1955 +Brug:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Session blev ikke fundet: '{}'</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Ugyldig IP-adresse '{}'</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName('{}') mislykkedes</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>Der blev ikke fundet nogen WSLA-sessioner.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>Fundet {} WSLC-session{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Afslutning af session mislykkedes: "{}"</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} afsluttede med: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>Id</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>Forfatter-PID</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Vist navn</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Ukendt Kommando: '{}'</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Ukendt kommando: '{}'</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Det påkrævede argument er ikke angivet: '{}'</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Argument er angivet flere gange end tilladt: '{}'</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Viser hjælp til den markerede kommando</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Der er angivet flere argumenter, der udelukker hinanden: '{}'</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>Argumentet {} kan kun bruges sammen med {}</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Brug: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>Kommando</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>indstillinger</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>Følgende kommandoaliasser er tilgængelige:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>Følgende kommandoer er tilgængelige:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>Følgende underkommandoer er tilgængelige:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>Følgende indstillinger er tilgængelige:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>Følgende argumenter er tilgængelige:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Du kan finde flere oplysninger om en bestemt kommando i hjælpeargumentet.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Argumentnavnet blev ikke genkendt for den aktuelle kommando: '{}'</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>Denne kommando kræver administratorrettigheder for at køre.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Angiv sessionen, der skal bruges</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Vedhæft til stdout/stderr af objektbeholderen</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Vedhæft til stdin, og hold den åben</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Angiv porten, der skal bruges</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>Objektbeholder-id</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Manglende argumentværdi: '{}'</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Argumentaliaset blev ikke genkendt for den aktuelle kommando: '{}'</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Ugyldig argumentangivelse: '{}'</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Det tilstødende flagalias blev ikke fundet: '{}'</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>Det tilstødende alias er ikke et flag: '{}'</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Ugyldig argumentangivelse: '{}'</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>Flagargumentet må ikke indeholde en tilstødende værdi: '{}'</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Der blev fundet et positionsargument, hvor ingen var forventet: '{}'</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Der mangler et argumentnavn på: '{}'</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Videresendte argumenter kunne ikke fortolkes med start ved argumentet: '{}'</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Der blev fundet et ugyldigt ekstra argument: '{}'</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Aliasargumenter med en værdi skal være sidste i aliaskæden: '{}'</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Copyright (c) Microsoft Corporation. Alle rettigheder forbeholdes.
2122 +Du kan finde oplysninger om beskyttelse af personlige oplysninger for dette produkt på https://aka.ms/privacy.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Ugyldigt billede: "{}"</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Ugyldigt navn: '{}'</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Stien er ikke absolut: "{}"</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Enheden blev ikke fundet: '{}'</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Ugyldige enhedsindstillinger: '{}'</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Ikke-understøttet enhedstype: '{}'</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>Enheden '{}' er i brug.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Både Dockerfile og Containerfile fundet. Brug -f til at vælge den fil, der skal bruges</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Kunne ikke åbne "{}": {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>Der blev ikke fundet nogen containerfil eller Docker-fil i "{}"</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/de-DE/Resources.resw
+568
@@ -1956,4 +1956,572 @@ Sie können auch über die Befehlspalette in VS Code selbst auf weitere VS Code
1956 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1957 <value>Visual Studio-Integration</value>
1958 </data>
1959 + <data name="MessageWslcUsage" xml:space="preserve">
1960 + <value>wslc - WSL Container CLI
1961 +Verwendung:
1962 + wslc --help</value>
1963 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1964 + </data>
1965 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1966 + <value>Sitzung nicht gefunden: '{}'</value>
1967 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1968 + </data>
1969 + <data name="MessageInvalidIp" xml:space="preserve">
1970 + <value>Ungültige IP-Adresse „{}“</value>
1971 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1972 + </data>
1973 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1974 + <value>Fehler bei OpenSessionByName('{}')</value>
1975 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1976 + </data>
1977 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1978 + <value>Keine WSLA-Sitzungen gefunden.</value>
1979 + </data>
1980 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1981 + <value>{} WSLC-Sitzung{} gefunden:</value>
1982 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1983 + </data>
1984 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1985 + <value>Fehler beim Beenden der Sitzung: „{}“</value>
1986 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1987 + </data>
1988 + <data name="MessageWslcShellExited" xml:space="preserve">
1989 + <value>{} beendet mit: {}</value>
1990 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1991 + </data>
1992 + <data name="MessageWslcHeaderId" xml:space="preserve">
1993 + <value>ID</value>
1994 + </data>
1995 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1996 + <value>Ersteller-PID</value>
1997 + </data>
1998 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1999 + <value>Anzeigename</value>
2000 + </data>
2001 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2002 + <value>Unbekannter Befehl: „{}“</value>
2003 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2004 + </data>
2005 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2006 + <value>Nicht erkannter Befehl: „{}“</value>
2007 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2008 + </data>
2009 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2010 + <value>Erforderliches Argument nicht angegeben: „{}“</value>
2011 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2012 + </data>
2013 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2014 + <value>Argument wurde öfter angegeben als zulässig: „{}“</value>
2015 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2016 + </data>
2017 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2018 + <value>Zeigt Hilfe zum ausgewählten Befehl an</value>
2019 + </data>
2020 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2021 + <value>Es wurden mehrere sich gegenseitig ausschließende Argumente angegeben: „{}“</value>
2022 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2023 + </data>
2024 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2025 + <value>Das Argument {} kann nur mit {} verwendet werden.</value>
2026 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2027 + </data>
2028 + <data name="WSLCCLI_Usage" xml:space="preserve">
2029 + <value>Verbrauch: {} {}</value>
2030 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2031 + </data>
2032 + <data name="WSLCCLI_Command" xml:space="preserve">
2033 + <value>Befehl</value>
2034 + </data>
2035 + <data name="WSLCCLI_Options" xml:space="preserve">
2036 + <value>Optionen</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2039 + <value>Die folgenden Befehlsaliase sind verfügbar:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2042 + <value>Folgende Befehle sind verfügbar:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2045 + <value>Folgende Unterbefehle sind verfügbar:</value>
2046 + </data>
2047 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2048 + <value>Die folgenden Optionen stehen zur Verfügung:</value>
2049 + </data>
2050 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2051 + <value>Die folgenden Argumente sind verfügbar:</value>
2052 + </data>
2053 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2054 + <value>Wenn Sie weitere Details zu einem bestimmten Befehl erfahren möchten, übergeben Sie ihm das Hilfe-Argument.</value>
2055 + </data>
2056 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2057 + <value>Argumentname wurde für den aktuellen Befehl nicht erkannt: „{}“</value>
2058 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2059 + </data>
2060 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2061 + <value>Zum Ausführen dieses Befehls sind Administratorberechtigungen erforderlich.</value>
2062 + </data>
2063 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2064 + <value>Sitzung angeben, die verwendet werden soll</value>
2065 + </data>
2066 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2067 + <value>An stdout/stderr des Containers anhängen</value>
2068 + </data>
2069 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2070 + <value>An stdin anfügen und offen lassen</value>
2071 + </data>
2072 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2073 + <value>Port angeben, der verwendet werden soll</value>
2074 + </data>
2075 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2076 + <value>Container-ID</value>
2077 + </data>
2078 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2079 + <value>Fehlender Argumentwert: „{}“</value>
2080 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2081 + </data>
2082 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2083 + <value>Der Argument-Alias wurde für den aktuellen Befehl nicht erkannt: „{}“</value>
2084 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2085 + </data>
2086 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2087 + <value>Ungültige Spezifikation des Arguments: „{}“</value>
2088 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2089 + </data>
2090 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2091 + <value>Angrenzender Kennzeichenalias nicht gefunden: „{}“</value>
2092 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2093 + </data>
2094 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2095 + <value>Angrenzender Alias ist kein Kennzeichen: „{}“</value>
2096 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2097 + </data>
2098 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2099 + <value>Ungültige Spezifikation des Arguments: „{}“</value>
2100 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2101 + </data>
2102 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2103 + <value>Kennzeichenargument darf keinen angrenzenden Wert enthalten: „{}“</value>
2104 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2105 + </data>
2106 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2107 + <value>Ein positionelles Argument wurde gefunden, obwohl keines erwartet wurde: „{}“</value>
2108 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2109 + </data>
2110 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2111 + <value>Fehlender Argumentname bei: „{}“</value>
2112 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2113 + </data>
2114 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2115 + <value>Fehler beim Auflösen weitergeleiteter Argumente, beginnend mit argument: "{}".</value>
2116 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2117 + </data>
2118 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2119 + <value>Ungültiges zusätzliches Argument gefunden: „{}“</value>
2120 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2121 + </data>
2122 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2123 + <value>Aliasargumente mit einem Wert müssen zuletzt in der Alias-Kette stehen: „{}“</value>
2124 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2125 + </data>
2126 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2127 + <value>Copyright (c) Microsoft Corporation. Alle Rechte vorbehalten.
2128 +Datenschutzinformationen zu diesem Produkt finden Sie unter https://aka.ms/privacy.</value>
2129 + <comment>Copyright notice and privacy link</comment>
2130 + </data>
2131 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2132 + <value>Ungültiges Bild: „{}“</value>
2133 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2134 + </data>
2135 + <data name="MessageWslcInvalidName" xml:space="preserve">
2136 + <value>Ungültiger Name: '{}'</value>
2137 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2138 + </data>
2139 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2140 + <value>Der Pfad ist nicht absolut: „{}“.</value>
2141 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2142 + </data>
2143 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2144 + <value>Volume nicht gefunden: '{}'</value>
2145 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2146 + </data>
2147 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2148 + <value>Ungültige Volumeoptionen: '{}'</value>
2149 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2150 + </data>
2151 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2152 + <value>Nicht unterstützter Volumetyp: '{}'</value>
2153 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2154 + </data>
2155 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2156 + <value>Volume '{}' wird verwendet.</value>
2157 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2158 + </data>
2159 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2160 + <value>Sowohl Dockerfile als auch Containerfile wurden gefunden. Verwenden Sie -f, um die Datei auszuwählen, die verwendet werden soll.</value>
2161 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2162 + </data>
2163 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2164 + <value>Fehler beim Öffnen von „{}“: {}</value>
2165 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2166 + </data>
2167 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2168 + <value>Kein Containerfile oder Dockerfile in „{}“ gefunden</value>
2169 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2170 + </data>
2171 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2172 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2173 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2174 + </data>
2175 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2176 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2177 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2178 + </data>
2179 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2180 + <value>Manage containers.</value>
2181 + </data>
2182 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2183 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2184 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2187 + <value>Attach to a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2190 + <value>Attaches to a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2193 + <value>Create a container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2196 + <value>Creates a container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2199 + <value>Execute a command in a running container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2202 + <value>Executes a command in a running container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2205 + <value>Inspect a container.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2208 + <value>Display detailed information about a container.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2211 + <value>Kill containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2214 + <value>Kills containers.</value>
2215 + </data>
2216 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2217 + <value>List containers.</value>
2218 + </data>
2219 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2220 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2221 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2224 + <value>View container logs.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2227 + <value>View logs for a container.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2230 + <value>Remove containers.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2233 + <value>Removes containers.</value>
2234 + </data>
2235 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2236 + <value>Run a container.</value>
2237 + </data>
2238 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2239 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2240 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2243 + <value>Start a container.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2246 + <value>Starts a container.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2249 + <value>Stop containers.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2252 + <value>Stops containers.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2255 + <value>Manage images.</value>
2256 + </data>
2257 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2258 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2259 + </data>
2260 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2261 + <value>Build an image from a Dockerfile.</value>
2262 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2263 + </data>
2264 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2265 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2266 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2267 + </data>
2268 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2269 + <value>Inspect images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2272 + <value>Inspect images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2275 + <value>List images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2278 + <value>Lists images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2281 + <value>Load images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2284 + <value>Loads images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2287 + <value>Pull images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2290 + <value>Pulls images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2293 + <value>Remove images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2296 + <value>Removes images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2299 + <value>Save images.</value>
2300 + </data>
2301 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2302 + <value>Saves images.</value>
2303 + </data>
2304 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2305 + <value>Manage sessions.</value>
2306 + </data>
2307 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2308 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2309 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2310 + </data>
2311 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2312 + <value>List sessions.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2315 + <value>Lists active session(s).</value>
2316 + </data>
2317 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2318 + <value>Attach to a session.</value>
2319 + </data>
2320 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2321 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2322 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2323 + </data>
2324 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2325 + <value>Terminate a session.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2328 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2329 + </data>
2330 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2331 + <value>Open the settings file in the default editor.</value>
2332 + </data>
2333 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2334 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2335 +On first run, creates the file with all settings commented out at their defaults.</value>
2336 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2339 + <value>Reset settings to built-in defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2342 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2343 + </data>
2344 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2345 + <value>Settings reset to defaults.</value>
2346 + </data>
2347 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2348 + <value>Show all regardless of state.</value>
2349 + </data>
2350 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2351 + <value>Set build-time variables (KEY=VALUE)</value>
2352 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2353 + </data>
2354 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2355 + <value>The command to run</value>
2356 + </data>
2357 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2358 + <value>Delete containers even if they are running</value>
2359 + </data>
2360 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2361 + <value>Run container in detached mode</value>
2362 + </data>
2363 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2364 + <value>Specifies the container init process executable</value>
2365 + </data>
2366 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2367 + <value>Key=Value pairs for environment variables</value>
2368 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2369 + </data>
2370 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2371 + <value>File containing key=value pairs of env variables</value>
2372 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2373 + </data>
2374 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2375 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2376 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2377 + </data>
2378 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2379 + <value>Follow log output</value>
2380 + </data>
2381 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2382 + <value>Output formatting (json or table) (Default: table)</value>
2383 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2384 + </data>
2385 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2386 + <value>Arguments to pass to container's init process</value>
2387 + </data>
2388 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2389 + <value>Delete images even if they are being used</value>
2390 + </data>
2391 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2392 + <value>Image name</value>
2393 + </data>
2394 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2395 + <value>Provides path to the tar archive file containing the image</value>
2396 + </data>
2397 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2398 + <value>Name of the container</value>
2399 + </data>
2400 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2401 + <value>Do not delete untagged parents</value>
2402 + </data>
2403 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2404 + <value>Do not truncate output</value>
2405 + </data>
2406 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2407 + <value>Path for the saved image</value>
2408 + </data>
2409 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2410 + <value>Path to the build context directory</value>
2411 + </data>
2412 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2413 + <value>Publish a port from a container to host</value>
2414 + </data>
2415 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2416 + <value>Outputs the container IDs only</value>
2417 + </data>
2418 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2419 + <value>Remove the container after it stops</value>
2420 + </data>
2421 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2422 + <value>Session ID</value>
2423 + </data>
2424 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2425 + <value>Signal to send (default: {})</value>
2426 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2427 + </data>
2428 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2429 + <value>Tag for the built image</value>
2430 + </data>
2431 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2432 + <value>Time in seconds to wait before executing (default 5)</value>
2433 + </data>
2434 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2435 + <value>Open a TTY with the container process.</value>
2436 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2437 + </data>
2438 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2439 + <value>Output verbose details</value>
2440 + </data>
2441 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2442 + <value>Show version information for this tool</value>
2443 + </data>
2444 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2445 + <value>Bind mount a volume to the container</value>
2446 + </data>
2447 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2448 + <value>Write the container ID to the provided path.</value>
2449 + </data>
2450 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2451 + <value>IP address of the DNS nameserver in resolv.conf</value>
2452 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2453 + </data>
2454 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2455 + <value>Set the default DNS Domain</value>
2456 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2457 + </data>
2458 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2459 + <value>Set DNS options</value>
2460 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2461 + </data>
2462 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2463 + <value>Set DNS search domains</value>
2464 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2465 + </data>
2466 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2467 + <value>Group Id for the process</value>
2468 + </data>
2469 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2470 + <value>No configuration of DNS in the container</value>
2471 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2472 + </data>
2473 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2474 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2475 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2476 + </data>
2477 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2478 + <value>Image pull policy (always|missing|never) (default:never)</value>
2479 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2480 + </data>
2481 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2482 + <value>Use this scheme for registry connection</value>
2483 + </data>
2484 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2485 + <value>Mount tmpfs to the container at the given path</value>
2486 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2487 + </data>
2488 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2489 + <value>User ID for the process (name|uid|uid:gid)</value>
2490 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2491 + </data>
2492 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2493 + <value>Expose virtualization capabilities to the container</value>
2494 + </data>
2495 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2496 + <value>Arguments to pass to the command being executed inside the container</value>
2497 + </data>
2498 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2499 + <value>Show detailed information about the listed sessions.</value>
2500 + </data>
2501 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2502 + <value>Invalid format type specified. Supported format types are: json, table</value>
2503 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2504 + </data>
2505 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2506 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2507 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2508 + </data>
2509 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2510 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2511 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2512 + </data>
2513 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2514 + <value>Image '{}' not found, pulling</value>
2515 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2516 + </data>
2517 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2518 + <value>Environment variable key cannot be empty</value>
2519 + </data>
2520 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2521 + <value>Environment variable key '{}' cannot contain whitespace</value>
2522 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2523 + </data>
2524 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2525 + <value>Requested load but no input provided.</value>
2526 + </data>
2527 </root>
\ No newline at end of file
localization/strings/en-GB/Resources.resw
+568
@@ -1950,4 +1950,572 @@ You can also access more VS Code Remote options through the command palette with
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Visual Studio Integration</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc - WSL Container CLI
1955 +Usage:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Session not found: '{}'</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Invalid IP address '{}'</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName('{}') failed</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>No WSLC sessions found.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>Found {} WSLC session{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Session termination failed: '{}'</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} exited with: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>ID</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>Creator PID</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Display Name</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Unknown command: '{}'</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Unrecognised command: '{}'</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Required argument not provided: '{}'</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Argument provided more times than allowed: '{}'</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Shows help about the selected command</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Multiple mutually exclusive arguments provided: '{}'</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>Argument {} can only be used with {}</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Usage: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>command</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>options</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>The following command aliases are available:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>The following commands are available:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>The following sub-commands are available:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>The following options are available:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>The following arguments are available:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>For more details on a specific command, pass it the help argument.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Argument name was not recognised for the current command: '{}'</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>This command requires administrator privileges to execute.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Specify the session to use</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Attach to stdout/stderr of the container</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Attach to stdin and keep it open</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Specify the port to use</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>Container ID</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Missing argument value: '{}'</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Argument alias was not recognised for the current command: '{}'</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Invalid argument specifier: '{}'</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Adjoined flag alias not found: '{}'</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>Adjoined alias is not a flag: '{}'</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Invalid argument specifier: '{}'</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>Flag argument cannot contain adjoined value: '{}'</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Found a positional argument when none was expected: '{}'</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Missing argument name at: '{}'</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Failed to resolve forwarded arguments starting at argument: '{}'</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Invalid extra argument encountered: '{}'</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Alias arguments with a value must be last in the alias chain: '{}'</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Copyright (c) Microsoft Corporation. All rights reserved.
2122 +For privacy information about this product please visit https://aka.ms/privacy.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Invalid image: '{}'</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Invalid name: '{}'</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Path is not absolute: '{}'</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Volume not found: '{}'</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Invalid volume options: '{}'</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Unsupported volume type: '{}'</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>Volume '{}' is in use.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Both Dockerfile and Containerfile found. Use -f to select the file to use</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Failed to open '{}': {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>No Containerfile or Dockerfile found in '{}'</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/en-US/Resources.resw
+916 -3
@@ -217,9 +217,6 @@ Install using 'wsl.exe {} &lt;Distro&gt;'.
217 To get a list of valid distributions, use 'wsl.exe --list --online'.</value>
218 <comment>{FixedPlaceholder="{}"}{Locked="--list "}{Locked="--online'"}Command line arguments, file names and string inserts should not be translated</comment>
219 </data>
220 - <data name="MessageInstanceTerminated" xml:space="preserve">
221 - <value>The Windows Subsystem for Linux instance has terminated.</value>
222 - </data>
220 <data name="MessageInvalidCommandLine" xml:space="preserve">
221 <value>Invalid command line argument: {}
222 Please use '{} --help' to get a list of supported arguments.</value>
@@ -858,6 +855,10 @@ For more information on Admin Protection, please visit https://aka.ms/apdevguide
855 <value>Failed to create the swap disk in '{}': {}</value>
856 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
857 </data>
858 + <data name="MessageFailedToCreateDisk" xml:space="preserve">
859 + <value>Failed to create disk '{}': {}</value>
860 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
861 + </data>
862 <data name="MessageNestedVirtualizationNotSupported" xml:space="preserve">
863 <value>Nested virtualization is not supported on this machine.</value>
864 </data>
@@ -1950,4 +1951,916 @@ You can also access more VS Code Remote options through the command palette with
1951 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1952 <value>Visual Studio Integration</value>
1953 </data>
1954 + <data name="MessageWslcUsage" xml:space="preserve">
1955 + <value>wslc - WSL Container CLI
1956 +Usage:
1957 + wslc --help</value>
1958 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1959 + </data>
1960 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1961 + <value>Session not found: '{}'</value>
1962 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1963 + </data>
1964 + <data name="MessageInvalidIp" xml:space="preserve">
1965 + <value>Invalid IP address '{}'</value>
1966 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1967 + </data>
1968 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1969 + <value>OpenSessionByName('{}') failed</value>
1970 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1971 + </data>
1972 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1973 + <value>No WSLC sessions found.</value>
1974 + </data>
1975 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1976 + <value>Found {} WSLC session{}:</value>
1977 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1978 + </data>
1979 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1980 + <value>Session termination failed: '{}'</value>
1981 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1982 + </data>
1983 + <data name="MessageWslcDefaultSessionNotFound" xml:space="preserve">
1984 + <value>Default session not found</value>
1985 + </data>
1986 + <data name="MessageWslcOpenDefaultSessionFailed" xml:space="preserve">
1987 + <value>Failed to open default session</value>
1988 + </data>
1989 + <data name="MessageWslcTerminateDefaultSessionFailed" xml:space="preserve">
1990 + <value>Default session termination failed</value>
1991 + </data>
1992 + <data name="MessageWslcShellExited" xml:space="preserve">
1993 + <value>{} exited with: {}</value>
1994 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1995 + </data>
1996 + <data name="MessageWslcCreatedSession" xml:space="preserve">
1997 + <value>Created session: '{}'</value>
1998 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1999 + </data>
2000 + <data name="MessageWslcHeaderId" xml:space="preserve">
2001 + <value>ID</value>
2002 + </data>
2003 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
2004 + <value>Creator PID</value>
2005 + </data>
2006 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
2007 + <value>Display Name</value>
2008 + </data>
2009 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2010 + <value>Unknown command: '{}'</value>
2011 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2012 + </data>
2013 + <data name="MessageWslcCannotRemoveRunningContainer" xml:space="preserve">
2014 + <value>Container '{}' is running and cannot be removed. Either stop the container before removing or use forced remove (-f).</value>
2015 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2016 + </data>
2017 + <data name="MessageWslcContainerNotRunning" xml:space="preserve">
2018 + <value>Container '{}' is not running.</value>
2019 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2020 + </data>
2021 + <data name="MessageWslcContainerIsRunning" xml:space="preserve">
2022 + <value>Container '{}' is running.</value>
2023 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2024 + </data>
2025 + <data name="MessageWslcContainerNotFound" xml:space="preserve">
2026 + <value>Container '{}' not found.</value>
2027 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2028 + </data>
2029 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2030 + <value>Unrecognized command: '{}'</value>
2031 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2032 + </data>
2033 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2034 + <value>Required argument not provided: '{}'</value>
2035 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2036 + </data>
2037 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2038 + <value>Argument provided more times than allowed: '{}'</value>
2039 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2040 + </data>
2041 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2042 + <value>Shows help about the selected command</value>
2043 + </data>
2044 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2045 + <value>Multiple mutually exclusive arguments provided: '{}'</value>
2046 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2047 + </data>
2048 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2049 + <value>Argument {} can only be used with {}</value>
2050 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2051 + </data>
2052 + <data name="WSLCCLI_Usage" xml:space="preserve">
2053 + <value>Usage: {} {}</value>
2054 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2055 + </data>
2056 + <data name="WSLCCLI_Command" xml:space="preserve">
2057 + <value>command</value>
2058 + </data>
2059 + <data name="WSLCCLI_Options" xml:space="preserve">
2060 + <value>options</value>
2061 + </data>
2062 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2063 + <value>The following command aliases are available:</value>
2064 + </data>
2065 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2066 + <value>The following commands are available:</value>
2067 + </data>
2068 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2069 + <value>The following sub-commands are available:</value>
2070 + </data>
2071 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2072 + <value>The following options are available:</value>
2073 + </data>
2074 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2075 + <value>The following arguments are available:</value>
2076 + </data>
2077 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2078 + <value>For more details on a specific command, pass it the help argument.</value>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2081 + <value>Argument name was not recognized for the current command: '{}'</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2085 + <value>This command requires administrator privileges to execute.</value>
2086 + </data>
2087 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2088 + <value>Specify the session to use</value>
2089 + </data>
2090 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2091 + <value>Attach to stdout/stderr of the container</value>
2092 + </data>
2093 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2094 + <value>Attach to stdin and keep it open</value>
2095 + </data>
2096 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2097 + <value>Specify the port to use</value>
2098 + </data>
2099 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2100 + <value>Container ID</value>
2101 + </data>
2102 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2103 + <value>Missing argument value: '{}'</value>
2104 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2105 + </data>
2106 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2107 + <value>Argument alias was not recognized for the current command: '{}'</value>
2108 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2109 + </data>
2110 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2111 + <value>Invalid argument specifier: '{}'</value>
2112 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2113 + </data>
2114 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2115 + <value>Adjoined flag alias not found: '{}'</value>
2116 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2117 + </data>
2118 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2119 + <value>Adjoined alias is not a flag: '{}'</value>
2120 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2121 + </data>
2122 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2123 + <value>Invalid argument specifier: '{}'</value>
2124 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2125 + </data>
2126 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2127 + <value>Flag argument cannot contain adjoined value: '{}'</value>
2128 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2129 + </data>
2130 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2131 + <value>Found a positional argument when none was expected: '{}'</value>
2132 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2133 + </data>
2134 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2135 + <value>Missing argument name at: '{}'</value>
2136 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2137 + </data>
2138 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2139 + <value>Failed to resolve forwarded arguments starting at argument: '{}'</value>
2140 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2141 + </data>
2142 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2143 + <value>Invalid extra argument encountered: '{}'</value>
2144 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2145 + </data>
2146 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2147 + <value>Alias arguments with a value must be last in the alias chain: '{}'</value>
2148 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2149 + </data>
2150 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2151 + <value>Copyright (c) Microsoft Corporation. All rights reserved.
2152 +For privacy information about this product please visit https://aka.ms/privacy.</value>
2153 + <comment>Copyright notice and privacy link</comment>
2154 + </data>
2155 + <data name = "MessageWslcInvalidImage" xml:space = "preserve" >
2156 + <value>Invalid image: '{}'</value>
2157 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2158 + </data>
2159 + <data name = "MessageWslcInvalidName" xml:space = "preserve" >
2160 + <value>Invalid name: '{}'</value>
2161 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2162 + </data>
2163 + <data name = "MessagePathNotAbsolute" xml:space = "preserve" >
2164 + <value>Path is not absolute: '{}'</value>
2165 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2166 + </data>
2167 + <data name = "MessageWslcVolumeNotFound" xml:space = "preserve" >
2168 + <value>Volume not found: '{}'</value>
2169 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2170 + </data>
2171 + <data name="MessageWslcImageNotFound" xml:space="preserve">
2172 + <value>Image '{}' not found.</value>
2173 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2174 + </data>
2175 + <data name = "MessageWslcMissingVolumeOption" xml:space = "preserve" >
2176 + <value>Missing required option: '{}'</value>
2177 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2178 + </data>
2179 + <data name = "MessageWslcInvalidVolumeType" xml:space = "preserve" >
2180 + <value>Unsupported volume type: '{}'</value>
2181 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2182 + </data>
2183 + <data name = "MessageWslcUnsupportedVolumeDriverOpts" xml:space = "preserve" >
2184 + <value>unsupported volume driver options: {}</value>
2185 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2186 + </data>
2187 + <data name = "MessageWslcVolumeInUse" xml:space = "preserve" >
2188 + <value>Volume '{}' is in use.</value>
2189 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2190 + </data>
2191 + <data name = "MessageWslcNetworkNotFound" xml:space = "preserve" >
2192 + <value>Network not found: '{}'</value>
2193 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2194 + </data>
2195 + <data name = "MessageWslcInvalidNetworkDriver" xml:space = "preserve" >
2196 + <value>Unsupported network driver: '{}'</value>
2197 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2198 + </data>
2199 + <data name = "MessageWslcNetworkInUse" xml:space = "preserve" >
2200 + <value>Network '{}' has active endpoints.</value>
2201 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2202 + </data>
2203 + <data name = "MessageWslcNetworkAlreadyExists" xml:space = "preserve" >
2204 + <value>Network '{}' already exists.</value>
2205 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2206 + </data>
2207 + <data name = "MessageWslcFailedToMountVolume" xml:space = "preserve" >
2208 + <value>Failed to create volume '{}': {}</value>
2209 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2210 + </data>
2211 + <data name = "MessageWslcPortInUse" xml:space = "preserve" >
2212 + <value>Port {} is already in use, cannot start container {}</value>
2213 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2214 + </data>
2215 + <data name = "MessageWslcBothDockerAndContainerFileFound" xml:space = "preserve" >
2216 + <value>Both Dockerfile and Containerfile found. Use -f to select the file to use</value>
2217 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2218 + </data>
2219 + <data name = "MessageWslcFailedToOpenFile" xml:space = "preserve" >
2220 + <value>Failed to open '{}': {}</value>
2221 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2222 + </data>
2223 + <data name = "MessageWslcBuildFileNotFound" xml:space = "preserve" >
2224 + <value>No Containerfile or Dockerfile found in '{}'</value>
2225 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2226 + </data>
2227 + <data name = "MessageWslcSessionStorageNotFound" xml:space = "preserve" >
2228 + <value>No WSLC session found in '{}'</value>
2229 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2230 + </data>
2231 + <data name="MessageWslcTagImageInvalidFormat" xml:space="preserve">
2232 + <value>Invalid image tag format: '{}'. Expected format is 'name:tag'</value>
2233 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2234 + </data>
2235 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2236 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2237 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2238 + </data>
2239 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2240 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2241 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2242 + </data>
2243 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2244 + <value>Manage containers.</value>
2245 + </data>
2246 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2247 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2248 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2249 + </data>
2250 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2251 + <value>Attach to a container.</value>
2252 + </data>
2253 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2254 + <value>Attaches to a container.</value>
2255 + </data>
2256 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2257 + <value>Create a container.</value>
2258 + </data>
2259 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2260 + <value>Creates a container.</value>
2261 + </data>
2262 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2263 + <value>Execute a command in a running container.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2266 + <value>Executes a command in a running container.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2269 + <value>Inspect a container.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2272 + <value>Display detailed information about a container.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2275 + <value>Kill containers.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2278 + <value>Kills containers.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2281 + <value>List containers.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2284 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2285 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2286 + </data>
2287 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2288 + <value>View container logs.</value>
2289 + </data>
2290 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2291 + <value>View logs for a container.</value>
2292 + </data>
2293 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2294 + <value>Remove containers.</value>
2295 + </data>
2296 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2297 + <value>Removes containers.</value>
2298 + </data>
2299 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2300 + <value>Run a container.</value>
2301 + </data>
2302 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2303 + <value>Runs a container. By default, the container is started in the foreground; use --detach to run in the background.</value>
2304 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2305 + </data>
2306 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2307 + <value>Start a container.</value>
2308 + </data>
2309 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2310 + <value>Starts a container.</value>
2311 + </data>
2312 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2313 + <value>Stop containers.</value>
2314 + </data>
2315 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2316 + <value>Stops containers.</value>
2317 + </data>
2318 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2319 + <value>Manage images.</value>
2320 + </data>
2321 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2322 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2323 + </data>
2324 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2325 + <value>Build an image from a Dockerfile.</value>
2326 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2327 + </data>
2328 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2329 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2330 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2333 + <value>Inspect images.</value>
2334 + </data>
2335 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2336 + <value>Inspect images.</value>
2337 + </data>
2338 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2339 + <value>List images.</value>
2340 + </data>
2341 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2342 + <value>Lists images.</value>
2343 + </data>
2344 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2345 + <value>Load images.</value>
2346 + </data>
2347 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2348 + <value>Loads images.</value>
2349 + </data>
2350 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2351 + <value>Pull images.</value>
2352 + </data>
2353 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2354 + <value>Pulls images.</value>
2355 + </data>
2356 + <data name="WSLCCLI_ImagePruneAllArgDescription" xml:space="preserve">
2357 + <value>Remove all unused images, not just dangling ones.</value>
2358 + </data>
2359 + <data name="WSLCCLI_ImagePruneDeleted" xml:space="preserve">
2360 + <value>Deleted: {}</value>
2361 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2362 + </data>
2363 + <data name="WSLCCLI_ImagePruneDesc" xml:space="preserve">
2364 + <value>Remove unused images.</value>
2365 + </data>
2366 + <data name="WSLCCLI_ImagePruneLongDesc" xml:space="preserve">
2367 + <value>Removes all dangling images. If --all is specified, removes all images not used by any container.</value>
2368 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2369 + </data>
2370 + <data name="WSLCCLI_ImagePruneSpaceReclaimed" xml:space="preserve">
2371 + <value>Total reclaimed space: {:.2f} MB</value>
2372 + <comment>{FixedPlaceholder="{:.2f}"}Command line arguments, file names and string inserts should not be translated</comment>
2373 + </data>
2374 + <data name="WSLCCLI_ImagePruneUntagged" xml:space="preserve">
2375 + <value>Untagged: {}</value>
2376 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2377 + </data>
2378 + <data name="WSLCCLI_ImagePushDesc" xml:space="preserve">
2379 + <value>Upload an image to a registry.</value>
2380 + </data>
2381 + <data name="WSLCCLI_ImagePushLongDesc" xml:space="preserve">
2382 + <value>Upload an image to a registry.</value>
2383 + </data>
2384 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2385 + <value>Remove images.</value>
2386 + </data>
2387 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2388 + <value>Removes images.</value>
2389 + </data>
2390 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2391 + <value>Save images.</value>
2392 + </data>
2393 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2394 + <value>Saves images.</value>
2395 + </data>
2396 + <data name="WSLCCLI_LoginDesc" xml:space="preserve">
2397 + <value>Log in to a registry.</value>
2398 + </data>
2399 + <data name="WSLCCLI_LoginLongDesc" xml:space="preserve">
2400 + <value>Log in to a registry. If no server is specified, the default is defined by the session.</value>
2401 + </data>
2402 + <data name="WSLCCLI_LogoutDesc" xml:space="preserve">
2403 + <value>Log out from a registry.</value>
2404 + </data>
2405 + <data name="WSLCCLI_LogoutLongDesc" xml:space="preserve">
2406 + <value>Log out from a registry. If no server is specified, the default is defined by the session.</value>
2407 + </data>
2408 + <data name="WSLCCLI_LoginSucceeded" xml:space="preserve">
2409 + <value>Login Succeeded</value>
2410 + </data>
2411 + <data name="WSLCCLI_LogoutSucceeded" xml:space="preserve">
2412 + <value>Removing login credentials for {}</value>
2413 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2414 + </data>
2415 + <data name="WSLCCLI_LogoutNotFound" xml:space="preserve">
2416 + <value>Not logged in to {}</value>
2417 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2418 + </data>
2419 + <data name="WSLCCLI_CredentialFileCorrupt" xml:space="preserve">
2420 + <value>Failed to parse credentials file '{}': the file may be corrupted.</value>
2421 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2422 + </data>
2423 + <data name="MessageWslcFailedToWriteFile" xml:space="preserve">
2424 + <value>Failed to write '{}': {}</value>
2425 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2426 + </data>
2427 + <data name="WSLCCLI_LoginServerArgDescription" xml:space="preserve">
2428 + <value>Server</value>
2429 + </data>
2430 + <data name="WSLCCLI_LoginUsernameArgDescription" xml:space="preserve">
2431 + <value>Username</value>
2432 + </data>
2433 + <data name="WSLCCLI_LoginPasswordArgDescription" xml:space="preserve">
2434 + <value>Password or Personal Access Token (PAT)</value>
2435 + <comment>{Locked="PAT"}Acronym should not be translated</comment>
2436 + </data>
2437 + <data name="WSLCCLI_LoginPasswordStdinArgDescription" xml:space="preserve">
2438 + <value>Take the Password or Personal Access Token (PAT) from stdin</value>
2439 + <comment>{Locked="PAT"}{Locked="stdin"}Technical terms should not be translated</comment>
2440 + </data>
2441 + <data name="WSLCCLI_LoginPasswordAndStdinMutuallyExclusive" xml:space="preserve">
2442 + <value>--password and --password-stdin are mutually exclusive</value>
2443 + <comment>{Locked="--password "}{Locked="--password-stdin "}Command line arguments, file names and string inserts should not be translated</comment>
2444 + </data>
2445 + <data name="WSLCCLI_LoginPasswordStdinRequiresUsername" xml:space="preserve">
2446 + <value>Must provide --username with --password-stdin</value>
2447 + <comment>{Locked="--username "}{Locked="--password-stdin"}Command line arguments, file names and string inserts should not be translated</comment>
2448 + </data>
2449 + <data name="WSLCCLI_LoginUsernamePrompt" xml:space="preserve">
2450 + <value>Username: </value>
2451 + </data>
2452 + <data name="WSLCCLI_LoginPasswordPrompt" xml:space="preserve">
2453 + <value>Password: </value>
2454 + </data>
2455 + <data name="WSLCCLI_RegistryCommandDesc" xml:space="preserve">
2456 + <value>Manage registry credentials.</value>
2457 + </data>
2458 + <data name="WSLCCLI_RegistryCommandLongDesc" xml:space="preserve">
2459 + <value>Manage registry credentials, including logging in and out of container registries.</value>
2460 + </data>
2461 + <data name="WSLCCLI_ImageTagDesc" xml:space="preserve">
2462 + <value>Tag an image.</value>
2463 + </data>
2464 + <data name="WSLCCLI_ImageTagLongDesc" xml:space="preserve">
2465 + <value>Tags an image.</value>
2466 + </data>
2467 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2468 + <value>Manage sessions.</value>
2469 + </data>
2470 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2471 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2472 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2473 + </data>
2474 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2475 + <value>List sessions.</value>
2476 + </data>
2477 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2478 + <value>Lists active session(s).</value>
2479 + </data>
2480 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2481 + <value>Attach to a session.</value>
2482 + </data>
2483 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2484 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2485 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2486 + </data>
2487 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2488 + <value>Terminate a session.</value>
2489 + </data>
2490 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2491 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2492 + </data>
2493 + <data name="WSLCCLI_SessionEnterDesc" xml:space="preserve">
2494 + <value>Enter a temporary session.</value>
2495 + </data>
2496 + <data name="WSLCCLI_SessionEnterLongDesc" xml:space="preserve">
2497 + <value>Creates a non-persistent session with the given storage path and opens a shell into it. The session is deleted when the shell exits. If no name is provided, a GUID is generated and printed to stderr.</value>
2498 + <comment>{Locked="GUID"}{Locked="stderr"}Technical terms should not be translated</comment>
2499 + </data>
2500 + <data name="WSLCCLI_SessionEnterNameArgDescription" xml:space="preserve">
2501 + <value>Name for the session. If not provided, a GUID is generated.</value>
2502 + <comment>{Locked="GUID"}Technical terms should not be translated</comment>
2503 + </data>
2504 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2505 + <value>Open the settings file in the default editor.</value>
2506 + </data>
2507 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2508 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2509 +On first run, creates the file with all settings commented out at their defaults.</value>
2510 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2511 + </data>
2512 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2513 + <value>Reset settings to built-in defaults.</value>
2514 + </data>
2515 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2516 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2517 + </data>
2518 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2519 + <value>Settings reset to defaults.</value>
2520 + </data>
2521 + <data name="WSLCCLI_VersionDesc" xml:space="preserve">
2522 + <value>Show version information.</value>
2523 + </data>
2524 + <data name="WSLCCLI_VersionLongDesc" xml:space="preserve">
2525 + <value>Show version information for this tool.</value>
2526 + </data>
2527 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2528 + <value>Show all regardless of state.</value>
2529 + </data>
2530 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2531 + <value>Set build-time variables (KEY=VALUE)</value>
2532 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2533 + </data>
2534 + <data name="WSLCCLI_BuildPullArgDescription" xml:space="preserve">
2535 + <value>Always attempt to pull a newer version of the image</value>
2536 + </data>
2537 + <data name="WSLCCLI_BuildTargetArgDescription" xml:space="preserve">
2538 + <value>Set the target build stage to build</value>
2539 + </data>
2540 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2541 + <value>The command to run</value>
2542 + </data>
2543 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2544 + <value>Delete containers even if they are running</value>
2545 + </data>
2546 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2547 + <value>Run container in detached mode</value>
2548 + </data>
2549 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2550 + <value>Specifies the container init process executable</value>
2551 + </data>
2552 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2553 + <value>Key=Value pairs for environment variables</value>
2554 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2555 + </data>
2556 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2557 + <value>File containing key=value pairs of env variables</value>
2558 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2559 + </data>
2560 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2561 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2562 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2563 + </data>
2564 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2565 + <value>Follow log output</value>
2566 + </data>
2567 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2568 + <value>Output formatting (json or table) (Default: table)</value>
2569 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2570 + </data>
2571 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2572 + <value>Arguments to pass to container's init process</value>
2573 + </data>
2574 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2575 + <value>Delete images even if they are being used</value>
2576 + </data>
2577 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2578 + <value>Image name</value>
2579 + </data>
2580 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2581 + <value>Provides path to the tar archive file containing the image</value>
2582 + </data>
2583 + <data name="WSLCCLI_LabelArgDescription" xml:space="preserve">
2584 + <value>Set metadata on an object</value>
2585 + </data>
2586 + <data name="WSLCCLI_LabelKeyEmptyError" xml:space="preserve">
2587 + <value>Label key cannot be empty</value>
2588 + </data>
2589 + <data name="WSLCCLI_HostnameArgDescription" xml:space="preserve">
2590 + <value>Container host name</value>
2591 + </data>
2592 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2593 + <value>Name of the container</value>
2594 + </data>
2595 + <data name="WSLCCLI_NoCacheArgDescription" xml:space="preserve">
2596 + <value>Do not use cache when building the image</value>
2597 + </data>
2598 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2599 + <value>Do not delete untagged parents</value>
2600 + </data>
2601 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2602 + <value>Do not truncate output</value>
2603 + </data>
2604 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2605 + <value>Path for the saved image</value>
2606 + </data>
2607 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2608 + <value>Path to the build context directory</value>
2609 + </data>
2610 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2611 + <value>Publish a port from a container to host</value>
2612 + </data>
2613 + <data name="WSLCCLI_PublishAllArgDescription" xml:space="preserve">
2614 + <value>Publish all exposed ports to random host ports</value>
2615 + </data>
2616 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2617 + <value>Outputs the container IDs only</value>
2618 + </data>
2619 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2620 + <value>Remove the container after it stops</value>
2621 + </data>
2622 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2623 + <value>Session ID</value>
2624 + </data>
2625 + <data name="WSLCCLI_SessionStoragePositionalArgDescription" xml:space="preserve">
2626 + <value>Session storage path</value>
2627 + </data>
2628 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2629 + <value>Signal to send (default: {})</value>
2630 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2631 + </data>
2632 + <data name="WSLCCLI_SourceArgDescription" xml:space="preserve">
2633 + <value>Current or existing image reference in the image-name[:tag] format</value>
2634 + </data>
2635 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2636 + <value>Tag for the built image</value>
2637 + </data>
2638 + <data name="WSLCCLI_TargetArgDescription" xml:space="preserve">
2639 + <value>New image reference in the image-name[:tag] format</value>
2640 + </data>
2641 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2642 + <value>Time in seconds to wait before executing (default 5)</value>
2643 + </data>
2644 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2645 + <value>Open a TTY with the container process.</value>
2646 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2647 + </data>
2648 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2649 + <value>Output verbose details</value>
2650 + </data>
2651 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2652 + <value>Show version information for this tool</value>
2653 + </data>
2654 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2655 + <value>Bind mount a volume to the container</value>
2656 + </data>
2657 + <data name="WSLCCLI_WorkingDirArgDescription" xml:space="preserve">
2658 + <value>Working directory inside the container</value>
2659 + </data>
2660 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2661 + <value>Write the container ID to the provided path.</value>
2662 + </data>
2663 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2664 + <value>IP address of the DNS nameserver in resolv.conf</value>
2665 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2666 + </data>
2667 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2668 + <value>Set the default DNS Domain</value>
2669 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2670 + </data>
2671 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2672 + <value>Set DNS options</value>
2673 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2674 + </data>
2675 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2676 + <value>Set DNS search domains</value>
2677 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2678 + </data>
2679 + <data name="WSLCCLI_DomainnameArgDescription" xml:space="preserve">
2680 + <value>Container domain name</value>
2681 + </data>
2682 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2683 + <value>Group Id for the process</value>
2684 + </data>
2685 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2686 + <value>No configuration of DNS in the container</value>
2687 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2688 + </data>
2689 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2690 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2691 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2692 + </data>
2693 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2694 + <value>Image pull policy (always|missing|never) (default:never)</value>
2695 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2696 + </data>
2697 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2698 + <value>Use this scheme for registry connection</value>
2699 + </data>
2700 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2701 + <value>Mount tmpfs to the container at the given path</value>
2702 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2703 + </data>
2704 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2705 + <value>User ID for the process (name|uid|uid:gid)</value>
2706 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2707 + </data>
2708 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2709 + <value>Expose virtualization capabilities to the container</value>
2710 + </data>
2711 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2712 + <value>Arguments to pass to the command being executed inside the container</value>
2713 + </data>
2714 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2715 + <value>Show detailed information about the listed sessions.</value>
2716 + </data>
2717 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2718 + <value>Invalid format type specified. Supported format types are: json, table</value>
2719 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2720 + </data>
2721 + <data name="WSLCCLI_InvalidInspectError" xml:space="preserve">
2722 + <value>Invalid {} value: {} is not a recognized inspect type. Supported inspect types are: {}.</value>
2723 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2724 + </data>
2725 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2726 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2727 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2728 + </data>
2729 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2730 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2731 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2732 + </data>
2733 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2734 + <value>Image '{}' not found, pulling</value>
2735 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2736 + </data>
2737 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2738 + <value>Environment variable key cannot be empty</value>
2739 + </data>
2740 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2741 + <value>Environment variable key '{}' cannot contain whitespace</value>
2742 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2743 + </data>
2744 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2745 + <value>Requested load but no input provided.</value>
2746 + </data>
2747 + <data name="WSLCCLI_VolumeCommandDesc" xml:space="preserve">
2748 + <value>Manage volumes.</value>
2749 + </data>
2750 + <data name="WSLCCLI_VolumeCommandLongDesc" xml:space="preserve">
2751 + <value>Manage the lifecycle of WSL volumes, including creating, inspecting, listing, and deleting them.</value>
2752 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2753 + </data>
2754 + <data name="WSLCCLI_VolumeCreateDesc" xml:space="preserve">
2755 + <value>Create a volume.</value>
2756 + </data>
2757 + <data name="WSLCCLI_VolumeCreateLongDesc" xml:space="preserve">
2758 + <value>Creates a named volume that can be attached to containers.</value>
2759 + </data>
2760 + <data name="WSLCCLI_VolumeRemoveDesc" xml:space="preserve">
2761 + <value>Remove one or more volumes.</value>
2762 + </data>
2763 + <data name="WSLCCLI_VolumeRemoveLongDesc" xml:space="preserve">
2764 + <value>Removes one or more volumes. A volume cannot be removed if it is in use by a container.</value>
2765 + </data>
2766 + <data name="WSLCCLI_VolumeInspectDesc" xml:space="preserve">
2767 + <value>Display detailed information on one or more volumes.</value>
2768 + </data>
2769 + <data name="WSLCCLI_VolumeInspectLongDesc" xml:space="preserve">
2770 + <value>Display detailed information on one or more volumes.</value>
2771 + </data>
2772 + <data name="WSLCCLI_VolumeListDesc" xml:space="preserve">
2773 + <value>List volumes.</value>
2774 + </data>
2775 + <data name="WSLCCLI_VolumeListLongDesc" xml:space="preserve">
2776 + <value>Lists all volumes in the session.</value>
2777 + </data>
2778 + <data name="WSLCCLI_VolumeNameArgDescription" xml:space="preserve">
2779 + <value>Volume name</value>
2780 + </data>
2781 + <data name="WSLCCLI_DriverArgDescription" xml:space="preserve">
2782 + <value>Specify volume driver name (default {})</value>
2783 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2784 + </data>
2785 + <data name="WSLCCLI_OptionsArgDescription" xml:space="preserve">
2786 + <value>Set driver specific options</value>
2787 + </data>
2788 + <data name="WSLCCLI_VolumeListQuietArgDesc" xml:space="preserve">
2789 + <value>Outputs the volume names only</value>
2790 + </data>
2791 + <data name="WSLCCLI_ImageSaveStdoutIsTerminalError" xml:space="preserve">
2792 + <value>Cannot write image to terminal. Use the -o flag or redirect stdout.</value>
2793 + </data>
2794 + <data name="WSLCCLI_VolumeFormatUsage" xml:space="preserve">
2795 + <value>Expected format: &lt;host path | named volume&gt;:&lt;container path&gt;[:mode]</value>
2796 + <comment>Usage string for volume mount specification.</comment>
2797 + </data>
2798 + <data name="WSLCCLI_VolumeInvalidSpec" xml:space="preserve">
2799 + <value>Invalid volume specifications: '{}'. {}</value>
2800 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2801 + </data>
2802 + <data name="WSLCCLI_VolumeHostPathEmpty" xml:space="preserve">
2803 + <value>Invalid volume specifications: '{}'. Host path cannot be empty. {}</value>
2804 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2805 + </data>
2806 + <data name="WSLCCLI_VolumeContainerPathEmpty" xml:space="preserve">
2807 + <value>Invalid volume specifications: '{}'. Container path cannot be empty. {}</value>
2808 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2809 + </data>
2810 + <data name="WSLCCLI_VolumeContainerPathNotAbsolute" xml:space="preserve">
2811 + <value>Invalid volume specifications: '{}'. Container path must be an absolute path (starting with '/'). {}</value>
2812 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2813 + </data>
2814 + <data name="WSLCCLI_VolumeHostPathInvalid" xml:space="preserve">
2815 + <value>Invalid volume specifications: '{}'. Host path '{}' is not a valid Windows path.</value>
2816 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2817 + </data>
2818 + <data name="WSLCCLI_ObjectIdArgDescription" xml:space="preserve">
2819 + <value>Name or Id of any object type</value>
2820 + </data>
2821 + <data name="WSLCCLI_TypeArgDescription" xml:space="preserve">
2822 + <value>Type of the object to inspect</value>
2823 + </data>
2824 + <data name="WSLCCLI_InspectDesc" xml:space="preserve">
2825 + <value>Inspect objects.</value>
2826 + </data>
2827 + <data name="WSLCCLI_InspectLongDesc" xml:space="preserve">
2828 + <value>Inspect objects.</value>
2829 + </data>
2830 + <data name="WSLCCLI_ObjectNotFoundError" xml:space="preserve">
2831 + <value>Object not found: {}</value>
2832 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2833 + </data>
2834 + <data name="WSLCUserSettings_Warning_InvalidValue" xml:space="preserve">
2835 + <value>Warning: Invalid value for setting '{}' in {}:{}.</value>
2836 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2837 + </data>
2838 + <data name="WSLCUserSettings_Warning_InvalidType" xml:space="preserve">
2839 + <value>Warning: Invalid type for setting '{}' in {}:{}.</value>
2840 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2841 + </data>
2842 + <data name="WSLCUserSettings_Warning_NonStringKey" xml:space="preserve">
2843 + <value>Warning: Non-string key in section '{}' in {}:{}.</value>
2844 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2845 + </data>
2846 + <data name="WSLCUserSettings_Warning_UnknownSection" xml:space="preserve">
2847 + <value>Warning: Unknown setting section '{}' in {}:{}.</value>
2848 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2849 + </data>
2850 + <data name="WSLCUserSettings_Warning_UnknownKey" xml:space="preserve">
2851 + <value>Warning: Unknown setting '{}' in {}:{}.</value>
2852 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2853 + </data>
2854 + <data name="WSLCUserSettings_Warning_FailedToOpen" xml:space="preserve">
2855 + <value>Warning: Failed to open settings file at {}. Error: {}.</value>
2856 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2857 + </data>
2858 + <data name="WSLCUserSettings_Warning_ParseError" xml:space="preserve">
2859 + <value>Warning: Settings file at {} could not be parsed. Error: {}.</value>
2860 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2861 + </data>
2862 + <data name="WSLCUserSettings_Warning_InvalidStructure" xml:space="preserve">
2863 + <value>Warning: Settings file at {} is empty or has invalid structure. Expected a YAML mapping.</value>
2864 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2865 + </data>
2866 </root>
localization/strings/es-ES/Resources.resw
+568
@@ -1956,4 +1956,572 @@ También puedes acceder a más opciones remotas de VS Code mediante la paleta d
1956 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1957 <value>Integración de Visual Studio</value>
1958 </data>
1959 + <data name="MessageWslcUsage" xml:space="preserve">
1960 + <value>wslc: CLI de contenedor de WSL
1961 +Uso:
1962 + wslc --help</value>
1963 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1964 + </data>
1965 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1966 + <value>No se encontró la sesión: "{}"</value>
1967 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1968 + </data>
1969 + <data name="MessageInvalidIp" xml:space="preserve">
1970 + <value>Dirección IP “{}” no válida</value>
1971 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1972 + </data>
1973 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1974 + <value>Error de OpenSessionByName("{}")</value>
1975 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1976 + </data>
1977 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1978 + <value>No se encontraron sesiones WSLC.</value>
1979 + </data>
1980 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1981 + <value>Se encontró {} sesión de WSLC{}:</value>
1982 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1983 + </data>
1984 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1985 + <value>Error al terminar la sesión: “{}”</value>
1986 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1987 + </data>
1988 + <data name="MessageWslcShellExited" xml:space="preserve">
1989 + <value>{} salió con: {}</value>
1990 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1991 + </data>
1992 + <data name="MessageWslcHeaderId" xml:space="preserve">
1993 + <value>Id.</value>
1994 + </data>
1995 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1996 + <value>PID de creador</value>
1997 + </data>
1998 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1999 + <value>Nombre para mostrar</value>
2000 + </data>
2001 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2002 + <value>Comando desconocido: "{}"</value>
2003 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2004 + </data>
2005 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2006 + <value>Comando no reconocido: '{}'</value>
2007 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2008 + </data>
2009 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2010 + <value>No se proporcionó el argumento requerido: '{}'</value>
2011 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2012 + </data>
2013 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2014 + <value>Argumento proporcionado más veces de lo permitido: '{}'</value>
2015 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2016 + </data>
2017 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2018 + <value>Muestra la ayuda sobre el comando seleccionado</value>
2019 + </data>
2020 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2021 + <value>Se proporcionaron varios argumentos mutuamente excluyentes: '{}'</value>
2022 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2023 + </data>
2024 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2025 + <value>El argumento {} solo se puede usar con {}</value>
2026 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2027 + </data>
2028 + <data name="WSLCCLI_Usage" xml:space="preserve">
2029 + <value>Uso: {} {}</value>
2030 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2031 + </data>
2032 + <data name="WSLCCLI_Command" xml:space="preserve">
2033 + <value>comando</value>
2034 + </data>
2035 + <data name="WSLCCLI_Options" xml:space="preserve">
2036 + <value>opciones</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2039 + <value>Están disponibles los siguientes alias de comando:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2042 + <value>Los siguientes comandos están disponibles:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2045 + <value>Los siguientes subcomandos están disponibles:</value>
2046 + </data>
2047 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2048 + <value>Están disponibles las siguientes opciones:</value>
2049 + </data>
2050 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2051 + <value>Los siguientes argumentos están disponibles:</value>
2052 + </data>
2053 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2054 + <value>Para más información sobre un comando específico, pásalo el argumento de ayuda.</value>
2055 + </data>
2056 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2057 + <value>No se reconoció el nombre del argumento para el comando actual: '{}'</value>
2058 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2059 + </data>
2060 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2061 + <value>Este comando requiere que se ejecuten privilegios de administrador.</value>
2062 + </data>
2063 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2064 + <value>Especifica la sesión que se va a usar</value>
2065 + </data>
2066 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2067 + <value>Asociación a stdout/stderr del contenedor</value>
2068 + </data>
2069 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2070 + <value>Adjuntar a stdin y mantenerlo abierto</value>
2071 + </data>
2072 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2073 + <value>Especificar el puerto que se va a usar</value>
2074 + </data>
2075 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2076 + <value>Id. de contenedor</value>
2077 + </data>
2078 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2079 + <value>Falta el valor del argumento: '{}'</value>
2080 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2081 + </data>
2082 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2083 + <value>No se reconoció el alias del argumento para el comando actual: '{}'</value>
2084 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2085 + </data>
2086 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2087 + <value>Especificador de argumento no válido: '{}'</value>
2088 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2089 + </data>
2090 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2091 + <value>No se encontró el alias de la marca adyacente: '{}'</value>
2092 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2093 + </data>
2094 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2095 + <value>El alias adyacente no es una marca: '{}'</value>
2096 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2097 + </data>
2098 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2099 + <value>Especificador de argumento no válido: '{}'</value>
2100 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2101 + </data>
2102 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2103 + <value>El argumento de marca no puede contener el valor adyacente: '{}'</value>
2104 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2105 + </data>
2106 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2107 + <value>Se encontró un argumento posicional cuando no se esperaba ninguno: '{}'</value>
2108 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2109 + </data>
2110 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2111 + <value>Falta el nombre del argumento en: '{}'</value>
2112 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2113 + </data>
2114 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2115 + <value>No se pudieron resolver los argumentos reenviados a partir del argumento '{}'</value>
2116 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2117 + </data>
2118 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2119 + <value>Se encontró un argumento adicional no válido: '{}'</value>
2120 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2121 + </data>
2122 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2123 + <value>Los argumentos de alias con un valor deben ir al final de la cadena de alias: '{}'</value>
2124 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2125 + </data>
2126 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2127 + <value>Copyright (c) Microsoft Corporation. Todos los derechos reservados.
2128 +Para obtener información de privacidad sobre este producto, visita https://aka.ms/privacy.</value>
2129 + <comment>Copyright notice and privacy link</comment>
2130 + </data>
2131 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2132 + <value>Imagen no válida: "{}"</value>
2133 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2134 + </data>
2135 + <data name="MessageWslcInvalidName" xml:space="preserve">
2136 + <value>Nombre no válido: '{}'</value>
2137 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2138 + </data>
2139 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2140 + <value>La ruta de acceso no es absoluta: "{}"</value>
2141 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2142 + </data>
2143 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2144 + <value>Volumen no encontrado: '{}'</value>
2145 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2146 + </data>
2147 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2148 + <value>Opciones de volumen no válidas: '{}'</value>
2149 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2150 + </data>
2151 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2152 + <value>Tipo de volumen no compatible: '{}'</value>
2153 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2154 + </data>
2155 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2156 + <value>El volumen '{}' está en uso.</value>
2157 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2158 + </data>
2159 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2160 + <value>Se encontraron Dockerfile y Containerfile. Usar -f para seleccionar el archivo que se va a usar</value>
2161 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2162 + </data>
2163 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2164 + <value>No se pudo abrir “{}”: {}</value>
2165 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2166 + </data>
2167 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2168 + <value>No se encontró Containerfile ni Dockerfile en “{}”</value>
2169 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2170 + </data>
2171 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2172 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2173 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2174 + </data>
2175 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2176 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2177 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2178 + </data>
2179 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2180 + <value>Manage containers.</value>
2181 + </data>
2182 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2183 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2184 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2187 + <value>Attach to a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2190 + <value>Attaches to a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2193 + <value>Create a container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2196 + <value>Creates a container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2199 + <value>Execute a command in a running container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2202 + <value>Executes a command in a running container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2205 + <value>Inspect a container.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2208 + <value>Display detailed information about a container.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2211 + <value>Kill containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2214 + <value>Kills containers.</value>
2215 + </data>
2216 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2217 + <value>List containers.</value>
2218 + </data>
2219 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2220 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2221 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2224 + <value>View container logs.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2227 + <value>View logs for a container.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2230 + <value>Remove containers.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2233 + <value>Removes containers.</value>
2234 + </data>
2235 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2236 + <value>Run a container.</value>
2237 + </data>
2238 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2239 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2240 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2243 + <value>Start a container.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2246 + <value>Starts a container.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2249 + <value>Stop containers.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2252 + <value>Stops containers.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2255 + <value>Manage images.</value>
2256 + </data>
2257 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2258 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2259 + </data>
2260 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2261 + <value>Build an image from a Dockerfile.</value>
2262 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2263 + </data>
2264 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2265 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2266 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2267 + </data>
2268 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2269 + <value>Inspect images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2272 + <value>Inspect images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2275 + <value>List images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2278 + <value>Lists images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2281 + <value>Load images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2284 + <value>Loads images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2287 + <value>Pull images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2290 + <value>Pulls images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2293 + <value>Remove images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2296 + <value>Removes images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2299 + <value>Save images.</value>
2300 + </data>
2301 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2302 + <value>Saves images.</value>
2303 + </data>
2304 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2305 + <value>Manage sessions.</value>
2306 + </data>
2307 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2308 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2309 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2310 + </data>
2311 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2312 + <value>List sessions.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2315 + <value>Lists active session(s).</value>
2316 + </data>
2317 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2318 + <value>Attach to a session.</value>
2319 + </data>
2320 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2321 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2322 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2323 + </data>
2324 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2325 + <value>Terminate a session.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2328 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2329 + </data>
2330 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2331 + <value>Open the settings file in the default editor.</value>
2332 + </data>
2333 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2334 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2335 +On first run, creates the file with all settings commented out at their defaults.</value>
2336 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2339 + <value>Reset settings to built-in defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2342 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2343 + </data>
2344 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2345 + <value>Settings reset to defaults.</value>
2346 + </data>
2347 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2348 + <value>Show all regardless of state.</value>
2349 + </data>
2350 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2351 + <value>Set build-time variables (KEY=VALUE)</value>
2352 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2353 + </data>
2354 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2355 + <value>The command to run</value>
2356 + </data>
2357 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2358 + <value>Delete containers even if they are running</value>
2359 + </data>
2360 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2361 + <value>Run container in detached mode</value>
2362 + </data>
2363 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2364 + <value>Specifies the container init process executable</value>
2365 + </data>
2366 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2367 + <value>Key=Value pairs for environment variables</value>
2368 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2369 + </data>
2370 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2371 + <value>File containing key=value pairs of env variables</value>
2372 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2373 + </data>
2374 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2375 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2376 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2377 + </data>
2378 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2379 + <value>Follow log output</value>
2380 + </data>
2381 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2382 + <value>Output formatting (json or table) (Default: table)</value>
2383 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2384 + </data>
2385 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2386 + <value>Arguments to pass to container's init process</value>
2387 + </data>
2388 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2389 + <value>Delete images even if they are being used</value>
2390 + </data>
2391 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2392 + <value>Image name</value>
2393 + </data>
2394 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2395 + <value>Provides path to the tar archive file containing the image</value>
2396 + </data>
2397 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2398 + <value>Name of the container</value>
2399 + </data>
2400 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2401 + <value>Do not delete untagged parents</value>
2402 + </data>
2403 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2404 + <value>Do not truncate output</value>
2405 + </data>
2406 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2407 + <value>Path for the saved image</value>
2408 + </data>
2409 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2410 + <value>Path to the build context directory</value>
2411 + </data>
2412 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2413 + <value>Publish a port from a container to host</value>
2414 + </data>
2415 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2416 + <value>Outputs the container IDs only</value>
2417 + </data>
2418 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2419 + <value>Remove the container after it stops</value>
2420 + </data>
2421 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2422 + <value>Session ID</value>
2423 + </data>
2424 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2425 + <value>Signal to send (default: {})</value>
2426 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2427 + </data>
2428 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2429 + <value>Tag for the built image</value>
2430 + </data>
2431 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2432 + <value>Time in seconds to wait before executing (default 5)</value>
2433 + </data>
2434 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2435 + <value>Open a TTY with the container process.</value>
2436 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2437 + </data>
2438 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2439 + <value>Output verbose details</value>
2440 + </data>
2441 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2442 + <value>Show version information for this tool</value>
2443 + </data>
2444 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2445 + <value>Bind mount a volume to the container</value>
2446 + </data>
2447 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2448 + <value>Write the container ID to the provided path.</value>
2449 + </data>
2450 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2451 + <value>IP address of the DNS nameserver in resolv.conf</value>
2452 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2453 + </data>
2454 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2455 + <value>Set the default DNS Domain</value>
2456 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2457 + </data>
2458 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2459 + <value>Set DNS options</value>
2460 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2461 + </data>
2462 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2463 + <value>Set DNS search domains</value>
2464 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2465 + </data>
2466 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2467 + <value>Group Id for the process</value>
2468 + </data>
2469 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2470 + <value>No configuration of DNS in the container</value>
2471 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2472 + </data>
2473 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2474 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2475 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2476 + </data>
2477 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2478 + <value>Image pull policy (always|missing|never) (default:never)</value>
2479 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2480 + </data>
2481 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2482 + <value>Use this scheme for registry connection</value>
2483 + </data>
2484 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2485 + <value>Mount tmpfs to the container at the given path</value>
2486 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2487 + </data>
2488 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2489 + <value>User ID for the process (name|uid|uid:gid)</value>
2490 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2491 + </data>
2492 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2493 + <value>Expose virtualization capabilities to the container</value>
2494 + </data>
2495 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2496 + <value>Arguments to pass to the command being executed inside the container</value>
2497 + </data>
2498 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2499 + <value>Show detailed information about the listed sessions.</value>
2500 + </data>
2501 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2502 + <value>Invalid format type specified. Supported format types are: json, table</value>
2503 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2504 + </data>
2505 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2506 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2507 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2508 + </data>
2509 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2510 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2511 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2512 + </data>
2513 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2514 + <value>Image '{}' not found, pulling</value>
2515 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2516 + </data>
2517 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2518 + <value>Environment variable key cannot be empty</value>
2519 + </data>
2520 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2521 + <value>Environment variable key '{}' cannot contain whitespace</value>
2522 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2523 + </data>
2524 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2525 + <value>Requested load but no input provided.</value>
2526 + </data>
2527 </root>
\ No newline at end of file
localization/strings/fi-FI/Resources.resw
+568
@@ -1950,4 +1950,572 @@ Voit käyttää myös muita VS Code Remote -asetuksia VS Coden komentovalikoiman
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Visual Studion integrointi</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc – WSL-säilön komentorivityökalu
1955 +Käyttö:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Istuntoa ei löydy. '{}'</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Virheellinen IP-osoite {}</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName('{}') epäonnistui</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>WSLA-istuntoja ei löytynyt.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>Löydettiin {} WSLC-istunto{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Istunnon päättäminen epäonnistui: {}</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} päättyi arvoon: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>Tunnus</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>Tekijän prosessin tunnus</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Näyttönimi</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Tuntematon komento: '{}'</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Tunnistamaton komento: {}</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Pakollista argumenttia ei ole annettu: {}</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Argumentti annettu sallittua useammin: {}</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Näyttää valitun komennon ohjeen</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Useita toisensa poissulkevia argumentteja annettiin: {}</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>Argumenttia {} voi käyttää vain kohteen {} kanssa</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Käyttö: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>komento</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>asetukset</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>Seuraavat komentotunnukset ovat käytettävissä:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>Seuraavat komennot ovat saatavilla:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>Seuraavat alikomennot ovat saatavilla:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>Seuraavat vaihtoehdot ovat saatavilla:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>Seuraavat argumentit ovat saatavilla:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Jos haluat lisätietoja tietystä komennosta, siirrä se ohjeargumenttiin.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Argumentin nimeä ei tunnistettu nykyiselle komennolle: {}</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>Tämän komennon suorittaminen edellyttää järjestelmänvalvojan oikeuksia.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Määritä käytettävä istunto</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Liitä säilön stdout- ja stderr-kohteisiin</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Liitä stdin-kohteeseen ja pidä se auki</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Määritä käytettävä portti</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>Säilötunnus</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Argumentin arvo puuttuu: {}</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Argumentin aliasta ei tunnistettu nykyiselle komennolle: {}</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Virheellinen argumentin määrite: {}</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Liitettyä merkintäaliasta ei löytynyt: {}</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>Liitetty alias ei ole merkintä: {}</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Virheellinen argumentin määrite: {}</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>Merkintäargumentti ei voi sisältää liitettyä arvoa: {}</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Löydettiin positioargumentti, kun yhtäkään ei odotettu: {}</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Argumentin nimi puuttuu kohteesta: {}</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Välitettyjä argumentteja ei voitu ratkaista argumentista: {} alkaen</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Virheellinen lisäargumentti: {}</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Alias-argumenttien, joilla on arvo, on oltava viimeisenä alias-ketjussa: {}</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Tekijänoikeus (c) Microsoft Corporation. Kaikki oikeudet pidätetään.
2122 +Tuotteen tietosuojatiedot löytyvät osoitteesta https://aka.ms/privacy.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Virheellinen kuva: '{}'</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Virheellinen nimi: {}</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Polku ei ole absoluuttinen: '{}'</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Asemaa ei löydy: {}</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Virheelliset aseman asetukset: {}</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Aseman tyyppiä ei tueta: {}</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>Asema {} on käytössä.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Sekä Dockerfile että Containerfile löytyivät. Käytä -f valitaksesi käytettävän tiedoston</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Kohteen {} avaaminen epäonnistui: {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>Kohteesta {} ei löytynyt Containerfile- tai Dockerfile-tiedostoa</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/fr-FR/Resources.resw
+568
@@ -1957,4 +1957,572 @@ Vous pouvez également accéder à davantage d'options VS Code Remote via la pal
1957 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1958 <value>Intégration de Visual Studio</value>
1959 </data>
1960 + <data name="MessageWslcUsage" xml:space="preserve">
1961 + <value>wslc – CLI du conteneur WSL
1962 +Utilisation :
1963 + wslc --help</value>
1964 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1965 + </data>
1966 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1967 + <value>Session introuvable : « {} »</value>
1968 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1969 + </data>
1970 + <data name="MessageInvalidIp" xml:space="preserve">
1971 + <value>Adresse IP non valide '{}'</value>
1972 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1973 + </data>
1974 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1975 + <value>Échec de OpenSessionByName('{}')</value>
1976 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1979 + <value>Aucune session WSLC n’a été trouvée.</value>
1980 + </data>
1981 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1982 + <value>Session WSLC {} trouvée{} :</value>
1983 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1984 + </data>
1985 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1986 + <value>Échec de la terminaison de la session : '{}'</value>
1987 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1988 + </data>
1989 + <data name="MessageWslcShellExited" xml:space="preserve">
1990 + <value>{} s’est arrêté avec : {}</value>
1991 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1992 + </data>
1993 + <data name="MessageWslcHeaderId" xml:space="preserve">
1994 + <value>ID</value>
1995 + </data>
1996 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1997 + <value>PID du créateur</value>
1998 + </data>
1999 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
2000 + <value>Nom complet</value>
2001 + </data>
2002 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2003 + <value>Commande inconnue : « {} »</value>
2004 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2005 + </data>
2006 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2007 + <value>Commande non reconnue : '{}'</value>
2008 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2009 + </data>
2010 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2011 + <value>Argument requis non fourni : '{}'</value>
2012 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2013 + </data>
2014 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2015 + <value>Argument fourni plus de fois que permis : '{}'</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2019 + <value>Affiche l’aide sur la commande sélectionnée</value>
2020 + </data>
2021 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2022 + <value>Plusieurs arguments mutuellement exclusifs fournis : '{}'</value>
2023 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2024 + </data>
2025 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2026 + <value>L’argument {} ne peut être utilisé qu’avec {}</value>
2027 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2028 + </data>
2029 + <data name="WSLCCLI_Usage" xml:space="preserve">
2030 + <value>Utilisation : {} {}</value>
2031 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2032 + </data>
2033 + <data name="WSLCCLI_Command" xml:space="preserve">
2034 + <value>commande</value>
2035 + </data>
2036 + <data name="WSLCCLI_Options" xml:space="preserve">
2037 + <value>options</value>
2038 + </data>
2039 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2040 + <value>Les alias de commande suivants sont disponibles :</value>
2041 + </data>
2042 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2043 + <value>Les commandes suivantes sont disponibles :</value>
2044 + </data>
2045 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2046 + <value>Les sous-commandes suivantes sont disponibles :</value>
2047 + </data>
2048 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2049 + <value>Les options suivantes sont disponibles :</value>
2050 + </data>
2051 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2052 + <value>Les arguments suivants sont disponibles :</value>
2053 + </data>
2054 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2055 + <value>Pour en savoir plus sur une commande spécifique, passez-la à l’argument aide.</value>
2056 + </data>
2057 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2058 + <value>Le nom d’argument n’a pas été reconnu pour la commande actuelle : « {} »</value>
2059 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2060 + </data>
2061 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2062 + <value>Cette commande nécessite des privilèges d'administrateur pour être exécutée.</value>
2063 + </data>
2064 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2065 + <value>Spécifiez la session à utiliser</value>
2066 + </data>
2067 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2068 + <value>Se connecter à la sortie standard/à la sortie d'erreur standard du conteneur</value>
2069 + </data>
2070 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2071 + <value>Connectez-vous à l'entrée standard et laissez-la ouverte</value>
2072 + </data>
2073 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2074 + <value>Spécifiez le port à utiliser</value>
2075 + </data>
2076 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2077 + <value>ID de conteneur</value>
2078 + </data>
2079 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2080 + <value>Valeur d'argument manquante : '{}'</value>
2081 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2082 + </data>
2083 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2084 + <value>L’alias d’argument n’a pas été reconnu pour la commande actuelle : « {} »</value>
2085 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2086 + </data>
2087 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2088 + <value>Spécificateur d'argument invalide : '{}'</value>
2089 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2090 + </data>
2091 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2092 + <value>Alias ​​du drapeau adjacent introuvable : '{}'</value>
2093 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2094 + </data>
2095 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2096 + <value>L'alias adjacent n'est pas un drapeau : '{}'</value>
2097 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2098 + </data>
2099 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2100 + <value>Spécificateur d'argument invalide : '{}'</value>
2101 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2102 + </data>
2103 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2104 + <value>L'argument drapeau ne peut pas contenir de valeur adjacente : '{}'</value>
2105 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2106 + </data>
2107 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2108 + <value>J'ai trouvé un argument positionnel alors qu'on n'en attendait aucun : '{}'</value>
2109 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2110 + </data>
2111 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2112 + <value>Nom de l'argument manquant à : '{}'</value>
2113 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2114 + </data>
2115 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2116 + <value>Échec de la résolution des arguments transférés à partir de l’argument : « {} »</value>
2117 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2118 + </data>
2119 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2120 + <value>Argument supplémentaire invalide rencontré : '{}'</value>
2121 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2122 + </data>
2123 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2124 + <value>Les arguments d’alias avec une valeur doivent être les derniers dans la chaîne d’alias : « {} »</value>
2125 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2126 + </data>
2127 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2128 + <value>Copyright (c) Microsoft Corporation. Tous droits réservés.
2129 +Pour plus d’informations sur la confidentialité de ce produit, veuillez consulter https://aka.ms/privacy.</value>
2130 + <comment>Copyright notice and privacy link</comment>
2131 + </data>
2132 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2133 + <value>Image non valide : « {} »</value>
2134 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2135 + </data>
2136 + <data name="MessageWslcInvalidName" xml:space="preserve">
2137 + <value>Nom non valide : '{}'</value>
2138 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2139 + </data>
2140 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2141 + <value>Le chemin n’est pas absolu : « {} »</value>
2142 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2143 + </data>
2144 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2145 + <value>Volume introuvable : '{}'</value>
2146 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2147 + </data>
2148 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2149 + <value>Options de volume non valides : '{}'</value>
2150 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2151 + </data>
2152 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2153 + <value>Type de volume non pris en charge : '{}'</value>
2154 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2155 + </data>
2156 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2157 + <value>Le volume '{}' est en cours d’utilisation.</value>
2158 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2159 + </data>
2160 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2161 + <value>Dockerfile et Containerfile ont été trouvés. Utilisez -f pour choisir le fichier à utiliser</value>
2162 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2163 + </data>
2164 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2165 + <value>Échec de l’ouverture '{}' : {}</value>
2166 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2167 + </data>
2168 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2169 + <value>Aucun Containerfile ni Dockerfile trouvé dans '{}'</value>
2170 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2171 + </data>
2172 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2173 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2174 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2175 + </data>
2176 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2177 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2178 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2181 + <value>Manage containers.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2184 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2185 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2186 + </data>
2187 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2188 + <value>Attach to a container.</value>
2189 + </data>
2190 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2191 + <value>Attaches to a container.</value>
2192 + </data>
2193 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2194 + <value>Create a container.</value>
2195 + </data>
2196 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2197 + <value>Creates a container.</value>
2198 + </data>
2199 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2200 + <value>Execute a command in a running container.</value>
2201 + </data>
2202 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2203 + <value>Executes a command in a running container.</value>
2204 + </data>
2205 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2206 + <value>Inspect a container.</value>
2207 + </data>
2208 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2209 + <value>Display detailed information about a container.</value>
2210 + </data>
2211 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2212 + <value>Kill containers.</value>
2213 + </data>
2214 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2215 + <value>Kills containers.</value>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2218 + <value>List containers.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2221 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2222 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2223 + </data>
2224 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2225 + <value>View container logs.</value>
2226 + </data>
2227 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2228 + <value>View logs for a container.</value>
2229 + </data>
2230 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2231 + <value>Remove containers.</value>
2232 + </data>
2233 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2234 + <value>Removes containers.</value>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2237 + <value>Run a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2240 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2241 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2242 + </data>
2243 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2244 + <value>Start a container.</value>
2245 + </data>
2246 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2247 + <value>Starts a container.</value>
2248 + </data>
2249 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2250 + <value>Stop containers.</value>
2251 + </data>
2252 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2253 + <value>Stops containers.</value>
2254 + </data>
2255 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2256 + <value>Manage images.</value>
2257 + </data>
2258 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2259 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2260 + </data>
2261 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2262 + <value>Build an image from a Dockerfile.</value>
2263 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2264 + </data>
2265 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2266 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2267 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2268 + </data>
2269 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2270 + <value>Inspect images.</value>
2271 + </data>
2272 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2273 + <value>Inspect images.</value>
2274 + </data>
2275 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2276 + <value>List images.</value>
2277 + </data>
2278 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2279 + <value>Lists images.</value>
2280 + </data>
2281 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2282 + <value>Load images.</value>
2283 + </data>
2284 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2285 + <value>Loads images.</value>
2286 + </data>
2287 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2288 + <value>Pull images.</value>
2289 + </data>
2290 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2291 + <value>Pulls images.</value>
2292 + </data>
2293 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2294 + <value>Remove images.</value>
2295 + </data>
2296 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2297 + <value>Removes images.</value>
2298 + </data>
2299 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2300 + <value>Save images.</value>
2301 + </data>
2302 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2303 + <value>Saves images.</value>
2304 + </data>
2305 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2306 + <value>Manage sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2309 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2310 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2311 + </data>
2312 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2313 + <value>List sessions.</value>
2314 + </data>
2315 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2316 + <value>Lists active session(s).</value>
2317 + </data>
2318 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2319 + <value>Attach to a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2322 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2323 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2324 + </data>
2325 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2326 + <value>Terminate a session.</value>
2327 + </data>
2328 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2329 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2330 + </data>
2331 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2332 + <value>Open the settings file in the default editor.</value>
2333 + </data>
2334 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2335 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2336 +On first run, creates the file with all settings commented out at their defaults.</value>
2337 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2338 + </data>
2339 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2340 + <value>Reset settings to built-in defaults.</value>
2341 + </data>
2342 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2343 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2344 + </data>
2345 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2346 + <value>Settings reset to defaults.</value>
2347 + </data>
2348 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2349 + <value>Show all regardless of state.</value>
2350 + </data>
2351 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2352 + <value>Set build-time variables (KEY=VALUE)</value>
2353 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2354 + </data>
2355 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2356 + <value>The command to run</value>
2357 + </data>
2358 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2359 + <value>Delete containers even if they are running</value>
2360 + </data>
2361 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2362 + <value>Run container in detached mode</value>
2363 + </data>
2364 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2365 + <value>Specifies the container init process executable</value>
2366 + </data>
2367 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2368 + <value>Key=Value pairs for environment variables</value>
2369 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2370 + </data>
2371 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2372 + <value>File containing key=value pairs of env variables</value>
2373 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2374 + </data>
2375 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2376 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2377 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2380 + <value>Follow log output</value>
2381 + </data>
2382 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2383 + <value>Output formatting (json or table) (Default: table)</value>
2384 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2385 + </data>
2386 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2387 + <value>Arguments to pass to container's init process</value>
2388 + </data>
2389 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2390 + <value>Delete images even if they are being used</value>
2391 + </data>
2392 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2393 + <value>Image name</value>
2394 + </data>
2395 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2396 + <value>Provides path to the tar archive file containing the image</value>
2397 + </data>
2398 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2399 + <value>Name of the container</value>
2400 + </data>
2401 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2402 + <value>Do not delete untagged parents</value>
2403 + </data>
2404 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2405 + <value>Do not truncate output</value>
2406 + </data>
2407 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2408 + <value>Path for the saved image</value>
2409 + </data>
2410 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2411 + <value>Path to the build context directory</value>
2412 + </data>
2413 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2414 + <value>Publish a port from a container to host</value>
2415 + </data>
2416 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2417 + <value>Outputs the container IDs only</value>
2418 + </data>
2419 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2420 + <value>Remove the container after it stops</value>
2421 + </data>
2422 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2423 + <value>Session ID</value>
2424 + </data>
2425 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2426 + <value>Signal to send (default: {})</value>
2427 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2428 + </data>
2429 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2430 + <value>Tag for the built image</value>
2431 + </data>
2432 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2433 + <value>Time in seconds to wait before executing (default 5)</value>
2434 + </data>
2435 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2436 + <value>Open a TTY with the container process.</value>
2437 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2438 + </data>
2439 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2440 + <value>Output verbose details</value>
2441 + </data>
2442 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2443 + <value>Show version information for this tool</value>
2444 + </data>
2445 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2446 + <value>Bind mount a volume to the container</value>
2447 + </data>
2448 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2449 + <value>Write the container ID to the provided path.</value>
2450 + </data>
2451 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2452 + <value>IP address of the DNS nameserver in resolv.conf</value>
2453 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2454 + </data>
2455 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2456 + <value>Set the default DNS Domain</value>
2457 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2458 + </data>
2459 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2460 + <value>Set DNS options</value>
2461 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2462 + </data>
2463 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2464 + <value>Set DNS search domains</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2468 + <value>Group Id for the process</value>
2469 + </data>
2470 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2471 + <value>No configuration of DNS in the container</value>
2472 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2473 + </data>
2474 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2475 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2476 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2477 + </data>
2478 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2479 + <value>Image pull policy (always|missing|never) (default:never)</value>
2480 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2483 + <value>Use this scheme for registry connection</value>
2484 + </data>
2485 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2486 + <value>Mount tmpfs to the container at the given path</value>
2487 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2488 + </data>
2489 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2490 + <value>User ID for the process (name|uid|uid:gid)</value>
2491 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2492 + </data>
2493 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2494 + <value>Expose virtualization capabilities to the container</value>
2495 + </data>
2496 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2497 + <value>Arguments to pass to the command being executed inside the container</value>
2498 + </data>
2499 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2500 + <value>Show detailed information about the listed sessions.</value>
2501 + </data>
2502 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2503 + <value>Invalid format type specified. Supported format types are: json, table</value>
2504 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2505 + </data>
2506 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2507 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2508 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2509 + </data>
2510 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2511 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2512 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2513 + </data>
2514 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2515 + <value>Image '{}' not found, pulling</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2519 + <value>Environment variable key cannot be empty</value>
2520 + </data>
2521 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2522 + <value>Environment variable key '{}' cannot contain whitespace</value>
2523 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2524 + </data>
2525 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2526 + <value>Requested load but no input provided.</value>
2527 + </data>
2528 </root>
\ No newline at end of file
localization/strings/hu-HU/Resources.resw
+568
@@ -1950,4 +1950,572 @@ A VS Code-ban található parancskatalógusban további VS Code Remote beállít
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Visual Studio-integráció</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc - WSL Container CLI
1955 +Usage:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Session not found: '{}'</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Érvénytelen IP-cím: „{}”</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName('{}') failed</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>Nem találhatók WSLC-munkamenetek.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>{} talált WSLC-munkamenet{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>A munkamenet lezárása nem sikerült: {}</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} exited with: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>Azonosító</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>Létrehozó folyamatazonosítója</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Megjelenítendő név</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Unknown command: '{}'</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Ismeretlen parancs: {}</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Egy kötelező argumentum nincs megadva: {}</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Az argumentum az engedélyezettnél többször van megadva: {}</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>A kiválasztott parancs súgójának megjelenítése</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Több egymást kölcsönösen kizáró argumentum van megadva: {}</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>A(z) {} argumentum csak a következővel használható: {}</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Használat: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>parancs</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>beállítások</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>A következő parancs-aliasok érhetők el:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>A következő parancsok használhatók:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>A következő alparancsok használhatók:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>A következő kapcsolók használhatók:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>A következő argumentumok használhatók:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Ha további információra van szüksége egy adott parancsról, adja meg hozzá a súgó argumentumát.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Az aktuális parancs argumentumának neve nem ismerhető fel: {}</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>Ennek a parancsnak a végrehajtásához rendszergazdai jogosultságok szükségesek.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Adja meg a használni kívánt munkamenetet</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Csatolja a tároló stdout/stderr kimenetéhez</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Csatolja egy standard bemenethez és tartsa nyitva</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Adja meg a használandó portot</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>Tárolóazonosító</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Hiányzó argumentumérték: {}</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Az aktuális parancs argumentumának aliasa nem ismerhető fel: {}</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Érvénytelen argumentummegadás: {}</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>A szomszédossági jelölő aliasa nem található: {}</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>A szomszédossági alias nem jelölő: {}</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Érvénytelen argumentummegadás: {}</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>A jelölő argumentum nem tartalmazhat szomszédos értéket: {}</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Nem várt helyen pozícióhoz kötött argumentum található: {}</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Hiányzik az argumentum neve a következő helyen: {}</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Nem sikerült feloldani a továbbított argumentumokat a következő argumentumtól kezdődően: {}</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Érvénytelen extra argumentum észlelve: {}</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Az értékekkel rendelkező alias-argumentumoknak az aliaslánc végén kell szerepelniük: {}</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Szerzői jog (c) Microsoft Corporation. Minden jog fenntartva.
2122 +További információ a termékről: https://aka.ms/privacy.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Érvénytelen kép: '{}'</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Érvénytelen név: {}</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Az elérési út nem abszolút: '{}'</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Nem található kötet: {}</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Érvénytelen kötetbeállítások: {}</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Nem támogatott kötettípus: {}</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>A(z) {} kötet használatban van.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Dockerfile és Containerfile is található. Az -f billentyűkombinációval válassza ki a használni kívánt fájlt</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Nem sikerült megnyitni a következőt: {}: {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>Nem található Containerfile vagy Dockerfile a(z) {} helyen</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/it-IT/Resources.resw
+568
@@ -1956,4 +1956,572 @@ Puoi anche accedere a più opzioni remote di VS Code tramite il riquadro comandi
1956 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1957 <value>Integrazione di Visual Studio</value>
1958 </data>
1959 + <data name="MessageWslcUsage" xml:space="preserve">
1960 + <value>wslc - CLI contenitore WSL
1961 +Utilizzo:
1962 + wslc --help</value>
1963 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1964 + </data>
1965 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1966 + <value>Sessione non trovata: '{}'</value>
1967 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1968 + </data>
1969 + <data name="MessageInvalidIp" xml:space="preserve">
1970 + <value>Indirizzo IP '{}' non valido</value>
1971 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1972 + </data>
1973 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1974 + <value>OpenSessionByName('{}') non riuscito</value>
1975 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1976 + </data>
1977 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1978 + <value>Nessuna sessione WSLC trovata.</value>
1979 + </data>
1980 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1981 + <value>Trovata {} sessione WSLC{}:</value>
1982 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1983 + </data>
1984 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1985 + <value>Terminazione della sessione non riuscita: '{}'</value>
1986 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1987 + </data>
1988 + <data name="MessageWslcShellExited" xml:space="preserve">
1989 + <value>{} terminato con: {}</value>
1990 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1991 + </data>
1992 + <data name="MessageWslcHeaderId" xml:space="preserve">
1993 + <value>ID</value>
1994 + </data>
1995 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1996 + <value>PID creatore</value>
1997 + </data>
1998 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1999 + <value>Nome visualizzato</value>
2000 + </data>
2001 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2002 + <value>Comando sconosciuto: '{}'</value>
2003 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2004 + </data>
2005 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2006 + <value>Comando non riconosciuto: '{}'</value>
2007 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2008 + </data>
2009 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2010 + <value>Argomento obbligatorio non specificato: '{}'</value>
2011 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2012 + </data>
2013 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2014 + <value>Argomento specificato più volte di quanto consentito: '{}'</value>
2015 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2016 + </data>
2017 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2018 + <value>Mostra la guida per il comando selezionato</value>
2019 + </data>
2020 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2021 + <value>Sono stati specificati più argomenti che si escludono a vicenda: '{}'</value>
2022 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2023 + </data>
2024 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2025 + <value>L'argomento {} può essere usato solo con {}</value>
2026 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2027 + </data>
2028 + <data name="WSLCCLI_Usage" xml:space="preserve">
2029 + <value>Utilizzo: {} {}</value>
2030 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2031 + </data>
2032 + <data name="WSLCCLI_Command" xml:space="preserve">
2033 + <value>comando</value>
2034 + </data>
2035 + <data name="WSLCCLI_Options" xml:space="preserve">
2036 + <value>Opzioni</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2039 + <value>Sono disponibili gli alias di comando seguenti:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2042 + <value>Sono disponibili i seguenti comandi:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2045 + <value>Sono disponibili i seguenti comandi secondari:</value>
2046 + </data>
2047 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2048 + <value>Sono disponibili le seguenti opzioni:</value>
2049 + </data>
2050 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2051 + <value>Sono disponibili i seguenti argomenti:</value>
2052 + </data>
2053 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2054 + <value>Per altre informazioni su un comando specifico, passa alla Guida degli argomenti.</value>
2055 + </data>
2056 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2057 + <value>Il nome dell'argomento non è stato riconosciuto per il comando corrente: '{}'</value>
2058 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2059 + </data>
2060 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2061 + <value>Questo comando richiede i privilegi di amministratore per l'esecuzione.</value>
2062 + </data>
2063 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2064 + <value>Specifica la sessione da usare</value>
2065 + </data>
2066 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2067 + <value>Collega a stdout/stderr del contenitore</value>
2068 + </data>
2069 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2070 + <value>Collega a stdin e tienilo aperto</value>
2071 + </data>
2072 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2073 + <value>Specifica la porta da usare</value>
2074 + </data>
2075 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2076 + <value>ID contenitore</value>
2077 + </data>
2078 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2079 + <value>Valore argomento mancante: '{}'</value>
2080 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2081 + </data>
2082 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2083 + <value>Alias di argomento non riconosciuto per il comando corrente: '{}'</value>
2084 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2085 + </data>
2086 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2087 + <value>Identificatore argomento non valido: '{}'</value>
2088 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2089 + </data>
2090 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2091 + <value>Alias flag contiguo non trovato: '{}'</value>
2092 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2093 + </data>
2094 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2095 + <value>L'alias contiguo non è un flag: '{}'</value>
2096 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2097 + </data>
2098 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2099 + <value>Identificatore argomento non valido: '{}'</value>
2100 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2101 + </data>
2102 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2103 + <value>L'argomento flag non può contenere un valore contiguo: '{}'</value>
2104 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2105 + </data>
2106 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2107 + <value>È stato trovato un argomento posizionale nonostante non ne fosse previsto alcuno: '{}'</value>
2108 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2109 + </data>
2110 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2111 + <value>Nome dell'argomento mancante in: '{}'</value>
2112 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2113 + </data>
2114 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2115 + <value>Non è stato possibile risolvere gli argomenti inoltrati a partire dall'argomento: '{}'</value>
2116 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2117 + </data>
2118 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2119 + <value>È stato rilevato un argomento aggiuntivo non valido: '{}'</value>
2120 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2121 + </data>
2122 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2123 + <value>Gli argomenti alias con un valore devono essere gli ultimi nella catena di alias: '{}'</value>
2124 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2125 + </data>
2126 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2127 + <value>Copyright (c) Microsoft Corporation. Tutti i diritti sono riservati.
2128 +Per informazioni sulla privacy di questo prodotto, visita https://aka.ms/privacy.</value>
2129 + <comment>Copyright notice and privacy link</comment>
2130 + </data>
2131 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2132 + <value>Immagine non valida: '{}'</value>
2133 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2134 + </data>
2135 + <data name="MessageWslcInvalidName" xml:space="preserve">
2136 + <value>Nome non valido: '{}'</value>
2137 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2138 + </data>
2139 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2140 + <value>Il percorso non è assoluto: '{}'</value>
2141 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2142 + </data>
2143 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2144 + <value>Volume non trovato: '{}'</value>
2145 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2146 + </data>
2147 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2148 + <value>Opzioni di volume non valide: '{}'</value>
2149 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2150 + </data>
2151 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2152 + <value>Tipo di volume non supportato: '{}'</value>
2153 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2154 + </data>
2155 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2156 + <value>Il volume '{}' è in uso.</value>
2157 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2158 + </data>
2159 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2160 + <value>Sono stati trovati sia Dockerfile che Containerfile. Usa -f per selezionare il file da usare</value>
2161 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2162 + </data>
2163 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2164 + <value>Apertura di '{}' non riuscita: {}</value>
2165 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2166 + </data>
2167 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2168 + <value>Nessun Containerfile o Dockerfile trovato in '{}'</value>
2169 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2170 + </data>
2171 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2172 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2173 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2174 + </data>
2175 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2176 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2177 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2178 + </data>
2179 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2180 + <value>Manage containers.</value>
2181 + </data>
2182 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2183 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2184 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2187 + <value>Attach to a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2190 + <value>Attaches to a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2193 + <value>Create a container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2196 + <value>Creates a container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2199 + <value>Execute a command in a running container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2202 + <value>Executes a command in a running container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2205 + <value>Inspect a container.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2208 + <value>Display detailed information about a container.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2211 + <value>Kill containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2214 + <value>Kills containers.</value>
2215 + </data>
2216 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2217 + <value>List containers.</value>
2218 + </data>
2219 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2220 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2221 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2224 + <value>View container logs.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2227 + <value>View logs for a container.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2230 + <value>Remove containers.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2233 + <value>Removes containers.</value>
2234 + </data>
2235 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2236 + <value>Run a container.</value>
2237 + </data>
2238 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2239 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2240 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2243 + <value>Start a container.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2246 + <value>Starts a container.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2249 + <value>Stop containers.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2252 + <value>Stops containers.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2255 + <value>Manage images.</value>
2256 + </data>
2257 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2258 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2259 + </data>
2260 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2261 + <value>Build an image from a Dockerfile.</value>
2262 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2263 + </data>
2264 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2265 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2266 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2267 + </data>
2268 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2269 + <value>Inspect images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2272 + <value>Inspect images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2275 + <value>List images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2278 + <value>Lists images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2281 + <value>Load images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2284 + <value>Loads images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2287 + <value>Pull images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2290 + <value>Pulls images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2293 + <value>Remove images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2296 + <value>Removes images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2299 + <value>Save images.</value>
2300 + </data>
2301 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2302 + <value>Saves images.</value>
2303 + </data>
2304 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2305 + <value>Manage sessions.</value>
2306 + </data>
2307 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2308 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2309 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2310 + </data>
2311 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2312 + <value>List sessions.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2315 + <value>Lists active session(s).</value>
2316 + </data>
2317 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2318 + <value>Attach to a session.</value>
2319 + </data>
2320 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2321 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2322 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2323 + </data>
2324 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2325 + <value>Terminate a session.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2328 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2329 + </data>
2330 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2331 + <value>Open the settings file in the default editor.</value>
2332 + </data>
2333 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2334 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2335 +On first run, creates the file with all settings commented out at their defaults.</value>
2336 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2339 + <value>Reset settings to built-in defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2342 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2343 + </data>
2344 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2345 + <value>Settings reset to defaults.</value>
2346 + </data>
2347 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2348 + <value>Show all regardless of state.</value>
2349 + </data>
2350 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2351 + <value>Set build-time variables (KEY=VALUE)</value>
2352 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2353 + </data>
2354 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2355 + <value>The command to run</value>
2356 + </data>
2357 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2358 + <value>Delete containers even if they are running</value>
2359 + </data>
2360 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2361 + <value>Run container in detached mode</value>
2362 + </data>
2363 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2364 + <value>Specifies the container init process executable</value>
2365 + </data>
2366 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2367 + <value>Key=Value pairs for environment variables</value>
2368 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2369 + </data>
2370 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2371 + <value>File containing key=value pairs of env variables</value>
2372 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2373 + </data>
2374 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2375 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2376 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2377 + </data>
2378 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2379 + <value>Follow log output</value>
2380 + </data>
2381 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2382 + <value>Output formatting (json or table) (Default: table)</value>
2383 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2384 + </data>
2385 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2386 + <value>Arguments to pass to container's init process</value>
2387 + </data>
2388 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2389 + <value>Delete images even if they are being used</value>
2390 + </data>
2391 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2392 + <value>Image name</value>
2393 + </data>
2394 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2395 + <value>Provides path to the tar archive file containing the image</value>
2396 + </data>
2397 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2398 + <value>Name of the container</value>
2399 + </data>
2400 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2401 + <value>Do not delete untagged parents</value>
2402 + </data>
2403 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2404 + <value>Do not truncate output</value>
2405 + </data>
2406 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2407 + <value>Path for the saved image</value>
2408 + </data>
2409 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2410 + <value>Path to the build context directory</value>
2411 + </data>
2412 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2413 + <value>Publish a port from a container to host</value>
2414 + </data>
2415 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2416 + <value>Outputs the container IDs only</value>
2417 + </data>
2418 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2419 + <value>Remove the container after it stops</value>
2420 + </data>
2421 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2422 + <value>Session ID</value>
2423 + </data>
2424 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2425 + <value>Signal to send (default: {})</value>
2426 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2427 + </data>
2428 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2429 + <value>Tag for the built image</value>
2430 + </data>
2431 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2432 + <value>Time in seconds to wait before executing (default 5)</value>
2433 + </data>
2434 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2435 + <value>Open a TTY with the container process.</value>
2436 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2437 + </data>
2438 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2439 + <value>Output verbose details</value>
2440 + </data>
2441 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2442 + <value>Show version information for this tool</value>
2443 + </data>
2444 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2445 + <value>Bind mount a volume to the container</value>
2446 + </data>
2447 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2448 + <value>Write the container ID to the provided path.</value>
2449 + </data>
2450 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2451 + <value>IP address of the DNS nameserver in resolv.conf</value>
2452 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2453 + </data>
2454 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2455 + <value>Set the default DNS Domain</value>
2456 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2457 + </data>
2458 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2459 + <value>Set DNS options</value>
2460 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2461 + </data>
2462 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2463 + <value>Set DNS search domains</value>
2464 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2465 + </data>
2466 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2467 + <value>Group Id for the process</value>
2468 + </data>
2469 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2470 + <value>No configuration of DNS in the container</value>
2471 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2472 + </data>
2473 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2474 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2475 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2476 + </data>
2477 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2478 + <value>Image pull policy (always|missing|never) (default:never)</value>
2479 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2480 + </data>
2481 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2482 + <value>Use this scheme for registry connection</value>
2483 + </data>
2484 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2485 + <value>Mount tmpfs to the container at the given path</value>
2486 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2487 + </data>
2488 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2489 + <value>User ID for the process (name|uid|uid:gid)</value>
2490 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2491 + </data>
2492 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2493 + <value>Expose virtualization capabilities to the container</value>
2494 + </data>
2495 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2496 + <value>Arguments to pass to the command being executed inside the container</value>
2497 + </data>
2498 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2499 + <value>Show detailed information about the listed sessions.</value>
2500 + </data>
2501 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2502 + <value>Invalid format type specified. Supported format types are: json, table</value>
2503 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2504 + </data>
2505 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2506 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2507 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2508 + </data>
2509 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2510 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2511 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2512 + </data>
2513 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2514 + <value>Image '{}' not found, pulling</value>
2515 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2516 + </data>
2517 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2518 + <value>Environment variable key cannot be empty</value>
2519 + </data>
2520 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2521 + <value>Environment variable key '{}' cannot contain whitespace</value>
2522 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2523 + </data>
2524 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2525 + <value>Requested load but no input provided.</value>
2526 + </data>
2527 </root>
\ No newline at end of file
localization/strings/ja-JP/Resources.resw
+568
@@ -1956,4 +1956,572 @@ VS Code 自体のコマンド パレットから、より多くの VS Code リ
1956 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1957 <value>Visual Studio 統合</value>
1958 </data>
1959 + <data name="MessageWslcUsage" xml:space="preserve">
1960 + <value>wslc - WSL コンテナー CLI
1961 +使用:
1962 + wslc --help</value>
1963 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1964 + </data>
1965 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1966 + <value>セッションが見つかりません: '{}'</value>
1967 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1968 + </data>
1969 + <data name="MessageInvalidIp" xml:space="preserve">
1970 + <value>IP アドレス '{}' が無効です</value>
1971 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1972 + </data>
1973 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1974 + <value>OpenSessionByName('{}') が失敗しました</value>
1975 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1976 + </data>
1977 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1978 + <value>WSLC セッションが見つかりません。</value>
1979 + </data>
1980 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1981 + <value>{} WSLC セッション{} が見つかりました:</value>
1982 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1983 + </data>
1984 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1985 + <value>セッションの終了に失敗しました: '{}'</value>
1986 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1987 + </data>
1988 + <data name="MessageWslcShellExited" xml:space="preserve">
1989 + <value>{} が次の値で終了しました: {}</value>
1990 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1991 + </data>
1992 + <data name="MessageWslcHeaderId" xml:space="preserve">
1993 + <value>ID</value>
1994 + </data>
1995 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1996 + <value>作成者 PID</value>
1997 + </data>
1998 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1999 + <value>表示名</value>
2000 + </data>
2001 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2002 + <value>不明なコマンド: '{}'</value>
2003 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2004 + </data>
2005 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2006 + <value>認識されないコマンド: '{}'</value>
2007 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2008 + </data>
2009 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2010 + <value>必須の引数が指定されていません: '{}'</value>
2011 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2012 + </data>
2013 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2014 + <value>引数に指定された回数が許容回数を超えています: '{}'</value>
2015 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2016 + </data>
2017 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2018 + <value>選択したコマンドに関するヘルプを表示</value>
2019 + </data>
2020 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2021 + <value>複数の相互排他的引数が指定されました: '{}'</value>
2022 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2023 + </data>
2024 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2025 + <value>引数 {} は {} でのみ使用できます</value>
2026 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2027 + </data>
2028 + <data name="WSLCCLI_Usage" xml:space="preserve">
2029 + <value>使用: {} {}</value>
2030 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2031 + </data>
2032 + <data name="WSLCCLI_Command" xml:space="preserve">
2033 + <value>コマンド</value>
2034 + </data>
2035 + <data name="WSLCCLI_Options" xml:space="preserve">
2036 + <value>オプション</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2039 + <value>次のコマンド エイリアスを使用できます:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2042 + <value>使用できるコマンドは次のとおりです:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2045 + <value>次のサブコマンドを使用できます。</value>
2046 + </data>
2047 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2048 + <value>次のオプションを使用できます。</value>
2049 + </data>
2050 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2051 + <value>次の引数を使用できます。</value>
2052 + </data>
2053 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2054 + <value>特定のコマンドの詳細については、そのコマンドにヘルプ引数を渡します。</value>
2055 + </data>
2056 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2057 + <value>現在のコマンドの引数名が認識されませんでした: '{}'</value>
2058 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2059 + </data>
2060 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2061 + <value>このコマンドを実行するには、管理者権限が必要です。</value>
2062 + </data>
2063 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2064 + <value>使用するセッションを指定する</value>
2065 + </data>
2066 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2067 + <value>コンテナーの stdout/stderr にアタッチする</value>
2068 + </data>
2069 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2070 + <value>stdin にアタッチして開いたままにする</value>
2071 + </data>
2072 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2073 + <value>使用するポートを指定する</value>
2074 + </data>
2075 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2076 + <value>コンテナー ID</value>
2077 + </data>
2078 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2079 + <value>引数の値がありません: '{}'</value>
2080 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2081 + </data>
2082 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2083 + <value>引数の別名が現在のコマンドに対して認識されませんでした: '{}'</value>
2084 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2085 + </data>
2086 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2087 + <value>無効な引数指定子: '{}'</value>
2088 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2089 + </data>
2090 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2091 + <value>隣接フラグ エイリアスが見つかりません: '{}'</value>
2092 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2093 + </data>
2094 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2095 + <value>隣接エイリアスがフラグではありません: '{}'</value>
2096 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2097 + </data>
2098 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2099 + <value>無効な引数指定子: '{}'</value>
2100 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2101 + </data>
2102 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2103 + <value>フラグ引数に隣接する値を含めることはできません: '{}'</value>
2104 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2105 + </data>
2106 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2107 + <value>必要なものが見つからないときに位置指定引数が見つかりました: '{}'</value>
2108 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2109 + </data>
2110 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2111 + <value>以下に引数名がありません: '{}'</value>
2112 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2113 + </data>
2114 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2115 + <value>次の引数で始まる転送された引数を解決できませんでした: '{}'</value>
2116 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2117 + </data>
2118 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2119 + <value>無効な余分な引数が見つかりました: '{}'</value>
2120 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2121 + </data>
2122 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2123 + <value>値を持つエイリアス引数は、エイリアス チェーンの最後に指定する必要があります: '{}'</value>
2124 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2125 + </data>
2126 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2127 + <value>Copyright (c) Microsoft Corporation.All rights reserved.
2128 +この製品に関するプライバシー情報については、https://aka.ms/privacy にアクセスしてください。</value>
2129 + <comment>Copyright notice and privacy link</comment>
2130 + </data>
2131 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2132 + <value>無効な画像: '{}'</value>
2133 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2134 + </data>
2135 + <data name="MessageWslcInvalidName" xml:space="preserve">
2136 + <value>無効な名前です: '{}'</value>
2137 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2138 + </data>
2139 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2140 + <value>パスが絶対パスではありません: '{}'</value>
2141 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2142 + </data>
2143 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2144 + <value>ボリュームが見つかりません: '{}'</value>
2145 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2146 + </data>
2147 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2148 + <value>無効なボリューム オプション: '{}'</value>
2149 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2150 + </data>
2151 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2152 + <value>サポートされていないボリュームの種類: '{}'</value>
2153 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2154 + </data>
2155 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2156 + <value>ボリューム '{}' は使用中です。</value>
2157 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2158 + </data>
2159 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2160 + <value>Dockerfile と Containerfile の両方が見つかりました。-f を使用して使用するファイルを選択してください</value>
2161 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2162 + </data>
2163 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2164 + <value>'{}' を開けませんでした: {}</value>
2165 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2166 + </data>
2167 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2168 + <value>'{}' に Containerfile または Dockerfile が見つかりませんでした</value>
2169 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2170 + </data>
2171 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2172 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2173 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2174 + </data>
2175 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2176 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2177 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2178 + </data>
2179 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2180 + <value>Manage containers.</value>
2181 + </data>
2182 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2183 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2184 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2187 + <value>Attach to a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2190 + <value>Attaches to a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2193 + <value>Create a container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2196 + <value>Creates a container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2199 + <value>Execute a command in a running container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2202 + <value>Executes a command in a running container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2205 + <value>Inspect a container.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2208 + <value>Display detailed information about a container.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2211 + <value>Kill containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2214 + <value>Kills containers.</value>
2215 + </data>
2216 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2217 + <value>List containers.</value>
2218 + </data>
2219 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2220 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2221 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2224 + <value>View container logs.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2227 + <value>View logs for a container.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2230 + <value>Remove containers.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2233 + <value>Removes containers.</value>
2234 + </data>
2235 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2236 + <value>Run a container.</value>
2237 + </data>
2238 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2239 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2240 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2243 + <value>Start a container.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2246 + <value>Starts a container.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2249 + <value>Stop containers.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2252 + <value>Stops containers.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2255 + <value>Manage images.</value>
2256 + </data>
2257 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2258 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2259 + </data>
2260 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2261 + <value>Build an image from a Dockerfile.</value>
2262 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2263 + </data>
2264 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2265 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2266 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2267 + </data>
2268 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2269 + <value>Inspect images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2272 + <value>Inspect images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2275 + <value>List images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2278 + <value>Lists images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2281 + <value>Load images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2284 + <value>Loads images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2287 + <value>Pull images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2290 + <value>Pulls images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2293 + <value>Remove images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2296 + <value>Removes images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2299 + <value>Save images.</value>
2300 + </data>
2301 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2302 + <value>Saves images.</value>
2303 + </data>
2304 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2305 + <value>Manage sessions.</value>
2306 + </data>
2307 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2308 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2309 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2310 + </data>
2311 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2312 + <value>List sessions.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2315 + <value>Lists active session(s).</value>
2316 + </data>
2317 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2318 + <value>Attach to a session.</value>
2319 + </data>
2320 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2321 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2322 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2323 + </data>
2324 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2325 + <value>Terminate a session.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2328 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2329 + </data>
2330 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2331 + <value>Open the settings file in the default editor.</value>
2332 + </data>
2333 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2334 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2335 +On first run, creates the file with all settings commented out at their defaults.</value>
2336 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2339 + <value>Reset settings to built-in defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2342 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2343 + </data>
2344 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2345 + <value>Settings reset to defaults.</value>
2346 + </data>
2347 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2348 + <value>Show all regardless of state.</value>
2349 + </data>
2350 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2351 + <value>Set build-time variables (KEY=VALUE)</value>
2352 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2353 + </data>
2354 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2355 + <value>The command to run</value>
2356 + </data>
2357 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2358 + <value>Delete containers even if they are running</value>
2359 + </data>
2360 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2361 + <value>Run container in detached mode</value>
2362 + </data>
2363 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2364 + <value>Specifies the container init process executable</value>
2365 + </data>
2366 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2367 + <value>Key=Value pairs for environment variables</value>
2368 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2369 + </data>
2370 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2371 + <value>File containing key=value pairs of env variables</value>
2372 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2373 + </data>
2374 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2375 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2376 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2377 + </data>
2378 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2379 + <value>Follow log output</value>
2380 + </data>
2381 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2382 + <value>Output formatting (json or table) (Default: table)</value>
2383 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2384 + </data>
2385 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2386 + <value>Arguments to pass to container's init process</value>
2387 + </data>
2388 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2389 + <value>Delete images even if they are being used</value>
2390 + </data>
2391 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2392 + <value>Image name</value>
2393 + </data>
2394 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2395 + <value>Provides path to the tar archive file containing the image</value>
2396 + </data>
2397 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2398 + <value>Name of the container</value>
2399 + </data>
2400 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2401 + <value>Do not delete untagged parents</value>
2402 + </data>
2403 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2404 + <value>Do not truncate output</value>
2405 + </data>
2406 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2407 + <value>Path for the saved image</value>
2408 + </data>
2409 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2410 + <value>Path to the build context directory</value>
2411 + </data>
2412 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2413 + <value>Publish a port from a container to host</value>
2414 + </data>
2415 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2416 + <value>Outputs the container IDs only</value>
2417 + </data>
2418 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2419 + <value>Remove the container after it stops</value>
2420 + </data>
2421 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2422 + <value>Session ID</value>
2423 + </data>
2424 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2425 + <value>Signal to send (default: {})</value>
2426 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2427 + </data>
2428 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2429 + <value>Tag for the built image</value>
2430 + </data>
2431 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2432 + <value>Time in seconds to wait before executing (default 5)</value>
2433 + </data>
2434 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2435 + <value>Open a TTY with the container process.</value>
2436 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2437 + </data>
2438 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2439 + <value>Output verbose details</value>
2440 + </data>
2441 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2442 + <value>Show version information for this tool</value>
2443 + </data>
2444 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2445 + <value>Bind mount a volume to the container</value>
2446 + </data>
2447 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2448 + <value>Write the container ID to the provided path.</value>
2449 + </data>
2450 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2451 + <value>IP address of the DNS nameserver in resolv.conf</value>
2452 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2453 + </data>
2454 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2455 + <value>Set the default DNS Domain</value>
2456 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2457 + </data>
2458 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2459 + <value>Set DNS options</value>
2460 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2461 + </data>
2462 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2463 + <value>Set DNS search domains</value>
2464 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2465 + </data>
2466 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2467 + <value>Group Id for the process</value>
2468 + </data>
2469 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2470 + <value>No configuration of DNS in the container</value>
2471 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2472 + </data>
2473 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2474 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2475 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2476 + </data>
2477 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2478 + <value>Image pull policy (always|missing|never) (default:never)</value>
2479 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2480 + </data>
2481 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2482 + <value>Use this scheme for registry connection</value>
2483 + </data>
2484 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2485 + <value>Mount tmpfs to the container at the given path</value>
2486 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2487 + </data>
2488 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2489 + <value>User ID for the process (name|uid|uid:gid)</value>
2490 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2491 + </data>
2492 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2493 + <value>Expose virtualization capabilities to the container</value>
2494 + </data>
2495 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2496 + <value>Arguments to pass to the command being executed inside the container</value>
2497 + </data>
2498 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2499 + <value>Show detailed information about the listed sessions.</value>
2500 + </data>
2501 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2502 + <value>Invalid format type specified. Supported format types are: json, table</value>
2503 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2504 + </data>
2505 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2506 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2507 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2508 + </data>
2509 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2510 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2511 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2512 + </data>
2513 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2514 + <value>Image '{}' not found, pulling</value>
2515 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2516 + </data>
2517 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2518 + <value>Environment variable key cannot be empty</value>
2519 + </data>
2520 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2521 + <value>Environment variable key '{}' cannot contain whitespace</value>
2522 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2523 + </data>
2524 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2525 + <value>Requested load but no input provided.</value>
2526 + </data>
2527 </root>
\ No newline at end of file
localization/strings/ko-KR/Resources.resw
+568
@@ -1956,4 +1956,572 @@ VS Code 자체 내의 명령 팔레트를 통해 더 많은 VS Code 원격 옵
1956 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1957 <value>Visual Studio 통합</value>
1958 </data>
1959 + <data name="MessageWslcUsage" xml:space="preserve">
1960 + <value>wslc - WSL 컨테이너 CLI
1961 +사용량:
1962 + wslc --help</value>
1963 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1964 + </data>
1965 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1966 + <value>세션을 찾을 수 없음: '{}'</value>
1967 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1968 + </data>
1969 + <data name="MessageInvalidIp" xml:space="preserve">
1970 + <value>잘못된 IP 주소 '{}'</value>
1971 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1972 + </data>
1973 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1974 + <value>OpenSessionByName('{}') 실패</value>
1975 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1976 + </data>
1977 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1978 + <value>WSLC 세션을 찾을 수 없습니다.</value>
1979 + </data>
1980 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1981 + <value>WSLC 세션 {}개를 찾았습니다.{}:</value>
1982 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1983 + </data>
1984 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1985 + <value>세션 종료 실패: '{}'</value>
1986 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1987 + </data>
1988 + <data name="MessageWslcShellExited" xml:space="preserve">
1989 + <value>{}이(가) 다음과 함께 종료되었습니다. {}</value>
1990 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1991 + </data>
1992 + <data name="MessageWslcHeaderId" xml:space="preserve">
1993 + <value>ID</value>
1994 + </data>
1995 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1996 + <value>작성자 PID</value>
1997 + </data>
1998 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1999 + <value>표시 이름</value>
2000 + </data>
2001 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2002 + <value>알 수 없는 명령: '{}'</value>
2003 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2004 + </data>
2005 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2006 + <value>인식할 수 없는 명령: '{}'</value>
2007 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2008 + </data>
2009 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2010 + <value>필수 인수가 제공 되지 않음: '{}'</value>
2011 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2012 + </data>
2013 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2014 + <value>인수가 허용되는 횟수보다 더 많이 제공됨: '{}'</value>
2015 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2016 + </data>
2017 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2018 + <value>선택한 명령에 대한 도움말을 표시</value>
2019 + </data>
2020 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2021 + <value>여러 상호 배타적 인수가 제공됨: '{}'</value>
2022 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2023 + </data>
2024 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2025 + <value>{} 인수는 {}과(와) 함께만 사용할 수 있습니다.</value>
2026 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2027 + </data>
2028 + <data name="WSLCCLI_Usage" xml:space="preserve">
2029 + <value>사용량: {} {}</value>
2030 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2031 + </data>
2032 + <data name="WSLCCLI_Command" xml:space="preserve">
2033 + <value>명령</value>
2034 + </data>
2035 + <data name="WSLCCLI_Options" xml:space="preserve">
2036 + <value>옵션</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2039 + <value>다음 명령 별칭을 사용할 수 있습니다.</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2042 + <value>다음 명령을 사용할 수 있음</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2045 + <value>다음 부명령어는 사용 할 수 없음</value>
2046 + </data>
2047 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2048 + <value>다음 선택 사항을 사용할 수 있음</value>
2049 + </data>
2050 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2051 + <value>다음 인수를 사용할 수 있음</value>
2052 + </data>
2053 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2054 + <value>특정 명령에 대한 자세한 내용을 보려면 도움말 인수에 해당 명령을 전달합니다.</value>
2055 + </data>
2056 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2057 + <value>현재 명령에 대해 인수 이름을 인식할 수 없음: '{}'</value>
2058 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2059 + </data>
2060 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2061 + <value>이 명령을 실행하려면 관리자 권한이 필요합니다.</value>
2062 + </data>
2063 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2064 + <value>사용할 세션을 지정하세요.</value>
2065 + </data>
2066 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2067 + <value>컨테이너의 stdout/stderr에 연결합니다.</value>
2068 + </data>
2069 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2070 + <value>stdin에 연결하고 열린 상태로 유지하세요</value>
2071 + </data>
2072 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2073 + <value>사용할 포트를 지정하세요.</value>
2074 + </data>
2075 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2076 + <value>컨테이너 ID</value>
2077 + </data>
2078 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2079 + <value>인수 값이 누락됨: '{}'</value>
2080 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2081 + </data>
2082 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2083 + <value>현재 명령에 대해 인수 별칭을 인식할 수 없음: '{}'</value>
2084 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2085 + </data>
2086 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2087 + <value>잘못된 인수 특정자: '{}'</value>
2088 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2089 + </data>
2090 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2091 + <value>근접한 플래그 별칭 찾을 수 없음: '{}'</value>
2092 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2093 + </data>
2094 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2095 + <value>근접한 별칭이 플래그가 아님: '{}'</value>
2096 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2097 + </data>
2098 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2099 + <value>잘못된 인수 특정자: '{}'</value>
2100 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2101 + </data>
2102 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2103 + <value>플래그 인수는 근접하는 값을 포함할 수 없음: '{}'</value>
2104 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2105 + </data>
2106 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2107 + <value>필요한 항목이 없을 때 위치 인수를 찾음: '{}'</value>
2108 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2109 + </data>
2110 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2111 + <value>인수 이름이 없음: '{}'</value>
2112 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2113 + </data>
2114 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2115 + <value>'{}' 인수에서 시작하는 전달된 인수를 resolve 못했습니다.</value>
2116 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2117 + </data>
2118 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2119 + <value>잘못된 추가 인수가 발견됨: '{}'</value>
2120 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2121 + </data>
2122 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2123 + <value>값이 있는 별칭 인수는 별칭 체인 '{}'의 마지막이어야 합니다.</value>
2124 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2125 + </data>
2126 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2127 + <value>Copyright (c) Microsoft Corporation. All rights reserved.
2128 +이 제품의 개인 정보 보호에 관한 정보는 https://aka.ms/privacy에서 확인하세요.</value>
2129 + <comment>Copyright notice and privacy link</comment>
2130 + </data>
2131 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2132 + <value>잘못된 이미지: '{}'</value>
2133 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2134 + </data>
2135 + <data name="MessageWslcInvalidName" xml:space="preserve">
2136 + <value>잘못된 이름: '{}'</value>
2137 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2138 + </data>
2139 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2140 + <value>경로가 절대 경로가 아님: '{}'</value>
2141 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2142 + </data>
2143 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2144 + <value>볼륨을 찾을 수 없음: ‘{}’</value>
2145 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2146 + </data>
2147 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2148 + <value>잘못된 볼륨 옵션: '{}'</value>
2149 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2150 + </data>
2151 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2152 + <value>지원하지 않는 볼륨 유형: '{}'.</value>
2153 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2154 + </data>
2155 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2156 + <value>볼륨 '{}'을(를) 사용 중입니다.</value>
2157 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2158 + </data>
2159 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2160 + <value>Dockerfile과 Containerfile을 모두 찾았습니다. -f를 사용하여 사용할 파일 선택</value>
2161 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2162 + </data>
2163 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2164 + <value>'{}'을(를) 열지 못함: {}</value>
2165 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2166 + </data>
2167 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2168 + <value>'{}'에서 Containerfile 또는 Dockerfile을 찾을 수 없습니다.</value>
2169 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2170 + </data>
2171 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2172 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2173 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2174 + </data>
2175 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2176 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2177 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2178 + </data>
2179 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2180 + <value>Manage containers.</value>
2181 + </data>
2182 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2183 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2184 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2187 + <value>Attach to a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2190 + <value>Attaches to a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2193 + <value>Create a container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2196 + <value>Creates a container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2199 + <value>Execute a command in a running container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2202 + <value>Executes a command in a running container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2205 + <value>Inspect a container.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2208 + <value>Display detailed information about a container.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2211 + <value>Kill containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2214 + <value>Kills containers.</value>
2215 + </data>
2216 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2217 + <value>List containers.</value>
2218 + </data>
2219 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2220 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2221 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2224 + <value>View container logs.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2227 + <value>View logs for a container.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2230 + <value>Remove containers.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2233 + <value>Removes containers.</value>
2234 + </data>
2235 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2236 + <value>Run a container.</value>
2237 + </data>
2238 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2239 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2240 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2243 + <value>Start a container.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2246 + <value>Starts a container.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2249 + <value>Stop containers.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2252 + <value>Stops containers.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2255 + <value>Manage images.</value>
2256 + </data>
2257 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2258 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2259 + </data>
2260 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2261 + <value>Build an image from a Dockerfile.</value>
2262 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2263 + </data>
2264 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2265 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2266 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2267 + </data>
2268 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2269 + <value>Inspect images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2272 + <value>Inspect images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2275 + <value>List images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2278 + <value>Lists images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2281 + <value>Load images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2284 + <value>Loads images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2287 + <value>Pull images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2290 + <value>Pulls images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2293 + <value>Remove images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2296 + <value>Removes images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2299 + <value>Save images.</value>
2300 + </data>
2301 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2302 + <value>Saves images.</value>
2303 + </data>
2304 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2305 + <value>Manage sessions.</value>
2306 + </data>
2307 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2308 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2309 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2310 + </data>
2311 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2312 + <value>List sessions.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2315 + <value>Lists active session(s).</value>
2316 + </data>
2317 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2318 + <value>Attach to a session.</value>
2319 + </data>
2320 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2321 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2322 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2323 + </data>
2324 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2325 + <value>Terminate a session.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2328 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2329 + </data>
2330 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2331 + <value>Open the settings file in the default editor.</value>
2332 + </data>
2333 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2334 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2335 +On first run, creates the file with all settings commented out at their defaults.</value>
2336 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2339 + <value>Reset settings to built-in defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2342 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2343 + </data>
2344 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2345 + <value>Settings reset to defaults.</value>
2346 + </data>
2347 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2348 + <value>Show all regardless of state.</value>
2349 + </data>
2350 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2351 + <value>Set build-time variables (KEY=VALUE)</value>
2352 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2353 + </data>
2354 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2355 + <value>The command to run</value>
2356 + </data>
2357 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2358 + <value>Delete containers even if they are running</value>
2359 + </data>
2360 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2361 + <value>Run container in detached mode</value>
2362 + </data>
2363 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2364 + <value>Specifies the container init process executable</value>
2365 + </data>
2366 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2367 + <value>Key=Value pairs for environment variables</value>
2368 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2369 + </data>
2370 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2371 + <value>File containing key=value pairs of env variables</value>
2372 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2373 + </data>
2374 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2375 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2376 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2377 + </data>
2378 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2379 + <value>Follow log output</value>
2380 + </data>
2381 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2382 + <value>Output formatting (json or table) (Default: table)</value>
2383 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2384 + </data>
2385 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2386 + <value>Arguments to pass to container's init process</value>
2387 + </data>
2388 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2389 + <value>Delete images even if they are being used</value>
2390 + </data>
2391 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2392 + <value>Image name</value>
2393 + </data>
2394 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2395 + <value>Provides path to the tar archive file containing the image</value>
2396 + </data>
2397 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2398 + <value>Name of the container</value>
2399 + </data>
2400 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2401 + <value>Do not delete untagged parents</value>
2402 + </data>
2403 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2404 + <value>Do not truncate output</value>
2405 + </data>
2406 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2407 + <value>Path for the saved image</value>
2408 + </data>
2409 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2410 + <value>Path to the build context directory</value>
2411 + </data>
2412 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2413 + <value>Publish a port from a container to host</value>
2414 + </data>
2415 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2416 + <value>Outputs the container IDs only</value>
2417 + </data>
2418 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2419 + <value>Remove the container after it stops</value>
2420 + </data>
2421 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2422 + <value>Session ID</value>
2423 + </data>
2424 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2425 + <value>Signal to send (default: {})</value>
2426 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2427 + </data>
2428 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2429 + <value>Tag for the built image</value>
2430 + </data>
2431 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2432 + <value>Time in seconds to wait before executing (default 5)</value>
2433 + </data>
2434 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2435 + <value>Open a TTY with the container process.</value>
2436 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2437 + </data>
2438 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2439 + <value>Output verbose details</value>
2440 + </data>
2441 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2442 + <value>Show version information for this tool</value>
2443 + </data>
2444 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2445 + <value>Bind mount a volume to the container</value>
2446 + </data>
2447 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2448 + <value>Write the container ID to the provided path.</value>
2449 + </data>
2450 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2451 + <value>IP address of the DNS nameserver in resolv.conf</value>
2452 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2453 + </data>
2454 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2455 + <value>Set the default DNS Domain</value>
2456 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2457 + </data>
2458 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2459 + <value>Set DNS options</value>
2460 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2461 + </data>
2462 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2463 + <value>Set DNS search domains</value>
2464 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2465 + </data>
2466 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2467 + <value>Group Id for the process</value>
2468 + </data>
2469 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2470 + <value>No configuration of DNS in the container</value>
2471 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2472 + </data>
2473 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2474 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2475 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2476 + </data>
2477 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2478 + <value>Image pull policy (always|missing|never) (default:never)</value>
2479 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2480 + </data>
2481 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2482 + <value>Use this scheme for registry connection</value>
2483 + </data>
2484 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2485 + <value>Mount tmpfs to the container at the given path</value>
2486 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2487 + </data>
2488 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2489 + <value>User ID for the process (name|uid|uid:gid)</value>
2490 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2491 + </data>
2492 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2493 + <value>Expose virtualization capabilities to the container</value>
2494 + </data>
2495 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2496 + <value>Arguments to pass to the command being executed inside the container</value>
2497 + </data>
2498 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2499 + <value>Show detailed information about the listed sessions.</value>
2500 + </data>
2501 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2502 + <value>Invalid format type specified. Supported format types are: json, table</value>
2503 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2504 + </data>
2505 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2506 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2507 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2508 + </data>
2509 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2510 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2511 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2512 + </data>
2513 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2514 + <value>Image '{}' not found, pulling</value>
2515 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2516 + </data>
2517 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2518 + <value>Environment variable key cannot be empty</value>
2519 + </data>
2520 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2521 + <value>Environment variable key '{}' cannot contain whitespace</value>
2522 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2523 + </data>
2524 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2525 + <value>Requested load but no input provided.</value>
2526 + </data>
2527 </root>
\ No newline at end of file
localization/strings/nb-NO/Resources.resw
+568
@@ -1950,4 +1950,572 @@ Du kan også åpne flere eksterne alternativer for VS Code gjennom kommandopalet
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Visual Studio-integrasjon</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc – WSL-beholder-CLI
1955 +Bruk:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Finner ikke økt. {}</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Ugyldig IP-adresse «{}»</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName({}) mislyktes</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>Finner ingen WSLC-økter.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>Fant {} WSLC-økt{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Kunne ikke avslutte økten: {}</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} avsluttet med: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>ID</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>Oppretter-PID</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Visningsnavn</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Ukjent kommando: {}</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Ukjent kommando: »{}»</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Obligatorisk argument ikke oppgitt: «{}»</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Argumentet ble angitt flere ganger enn tillatt: «{}»</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Viser hjelp om den valgte kommandoen</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Flere gjensidig utelukkende argumenter er oppgitt: «{}»</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>Argumentet {} kan bare brukes med {}</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Bruk: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>kommando</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>alternativer</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>Følgende kommandoaliaser er tilgjengelig:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>Følgende kommandoer er tilgjengelige:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>Følgende underkommandoer er tilgjengelige:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>Følgende alternativer er tilgjengelige:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>Følgende argumenter er tilgjengelige:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Hvis du vil ha mer informasjon om en bestemt kommando, må du angi hjelp-argumentet.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Argumentnavnet ble ikke gjenkjent for gjeldende kommando: «{}»</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>Administratorrettigheter kreves for å kjøre denne kommandoen.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Angi økten som skal brukes</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Legg ved i stdout/stderr for beholderen</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Legg ved stdin og hold åpen</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Angi porten som skal brukes</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>Beholder-ID</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Manglende argumentverdi: «{}»</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Argumentaliaset ble ikke gjenkjent for gjeldende kommando: «{}»</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Ugyldig argumentspesifikasjon: «{}»</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Finner ikke tilstøtende flaggalias: «{}»</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>Tilstøtende alias er ikke et flagg: «{}»</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Ugyldig argumentspesifikasjon: «{}»</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>Flaggargumentet kan ikke inneholde tilstøtende verdi: «{}»</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Fant et plasseringsargument når det ikke var forventet: «{}»</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Manglende argumentnavn på: «{}»</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Kunne ikke løse videresendte argumenter med start på argument: «{}»</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Fant ugyldig ekstra argument: «{}»</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Aliasargumenter med en verdi må være sist i aliaskjeden: «{}»</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Copyright (c) Microsoft Corporation. Med enerett.
2122 +For personverninformasjon om dette produktet kan du gå til https://aka.ms/privacy.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Ugyldig avbildning: {}</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Ugyldig navn: «{}»</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Banen er ikke absolutt: {}</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Finner ikke volumet: «{}»</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Ugyldige volumalternativer: «{}»</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Ustøttet volumtype: «{}»</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>Volumet «{}» er i bruk.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Både Dockerfile og Containerfile ble funnet. Bruk -f for å velge hvilken fil som skal brukes</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Kan ikke åpne {}: {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>Finner ingen Containerfile eller Dockerfile i {}</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/nl-NL/Resources.resw
+568
@@ -1950,4 +1950,572 @@ U hebt ook toegang tot meer externe VS Code-opties via het opdrachtpalet in VS C
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Visual Studio-integratie</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc - WSL-container-CLI
1955 +Gebruik:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>De sessie is niet gevonden: '{}'</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Ongeldig IP-adres: '{}'</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName({}) is mislukt</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>Er zijn geen WSLC-sessies gevonden.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>Gevonden {} WSLC-sessie{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Sessiebeëindiging mislukt: '{}'</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} is afgesloten met: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>Id</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>PID van maker</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Weergavenaam</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Onbekende opdracht: '{}'</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Niet-herkende opdracht: '{}'</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Vereist argument niet ingevoerd: '{}'</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Argument vaker ingevoerd dan toegestaan: '{}'</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Toont informatie over de geselecteerde opdracht</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Meerdere elkaar uitsluitende argumenten opgegeven: '{}'</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>Argument {} kan alleen worden gebruikt met {}</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Gebruik: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>opdracht</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>opties</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>De volgende opdrachtaliassen zijn beschikbaar:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>De volgende opdrachten zijn beschikbaar:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>De volgende subopdrachten zijn beschikbaar:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>De volgende opties zijn beschikbaar:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>De volgende argumenten zijn beschikbaar:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Geef het Help-argument door voor meer informatie over een specifieke opdracht.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Argumentnaam is niet herkend voor de huidige opdracht: '{}'</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>Deze opdracht vereist beheerdersrechten om uit te voeren.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Geef de sessie op die je wilt gebruiken</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Koppelen aan stdout/stderr van de container</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Koppelen aan stdin en deze openhouden</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Geef de poort op die moet worden gebruikt</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>Container-id</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Ontbrekende argumentwaarde: '{}'</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Argumentalias is niet herkend voor de huidige opdracht: '{}'</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Aanduiding ongeldig argument: '{}'</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Verbonden vlagalias niet gevonden: '{}'</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>Verbonden alias is geen vlag: '{}'</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Aanduiding ongeldig argument: '{}'</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>Vlagargument mag geen verbonden waarde bevatten: '{}'</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Er is een positioneel argument gevonden, terwijl er geen werd verwacht: '{}'</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Ontbrekende argumentnaam bij: '{}'</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Kan doorgestuurde argumenten niet omzetten, te beginnen bij argument: {}</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Ongeldig extra argument aangetroffen: '{}'</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Aliasargumenten met een waarde moeten als laatste in de aliasreeks staan: '{}'</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Copyright (c) Microsoft Corporation. Alle rechten voorbehouden.
2122 +Ga naar https://aka.ms/privacy voor privacyinformatie over dit product.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Ongeldige afbeelding: '{}'</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Ongeldige naam: '{}'</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Pad is niet absoluut: '{}'</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Volume niet gevonden: '{}'</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Ongeldige volumeopties: '{}'</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Niet-ondersteund volumetype: '{}'</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>Volume '{}' is in gebruik.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Zowel Dockerfile als Containerfile gevonden. Gebruik -f om het bestand te selecteren dat je wilt gebruiken</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Openen mislukt '{}': {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>Geen Containerfile of Dockerfile gevonden in '{}'</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/pl-PL/Resources.resw
+568
@@ -1950,4 +1950,572 @@ Możesz również uzyskać dostęp do większej liczby opcji zdalnych programu V
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Integracja z programem Visual Studio</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc – interfejs wiersza polecenia kontenera WSL
1955 +Użycie:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Nie znaleziono sesji: „{}”</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Nieprawidłowy adres IP „{}”</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>Operacja OpenSessionByName('{}') nie powiodła się</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>Nie znaleziono sesji WSLC.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>Znaleziono {} sesj{} WSLC:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Nie udało się zakończyć sesji: „{}”</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>Operacja {} zakończyła działanie z kodem: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>Identyfikator</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>Identyfikator PID twórcy</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Nazwa wyświetlana</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Nieznane polecenie: „{}”</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Nierozpoznane polecenie: „{}”</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Nie podano wymaganego argumentu: „{}”</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Argument podano więcej razy, niż jest to dozwolone: „{}”</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Wyświetla zawartość pomocy dla wybranego polecenia</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Podano wiele wzajemnie wykluczających się argumentów: „{}”</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>Argumentu {} można używać tylko z {}</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Użycie: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>polecenie</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>opcje</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>Dostępne są następujące aliasy poleceń:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>Dostępne są następujące polecenia:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>Dostępne są następujące polecenia podrzędne:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>Dostępne są następujące opcje:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>Dostępne są następujące argumenty:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Aby poznać szczegóły danego polecenia, podaj argument pomocy.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Nazwa argumentu nie została rozpoznana dla bieżącego polecenia: „{}”</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>To polecenie wymaga uprawnień administratora do wykonania.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Określ sesję, której chcesz użyć</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Dołącz do strumieni stdout/stderr kontenera</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Dołącz do strumienia stdin i pozostaw go otwartym</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Określ port, którego chcesz użyć</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>Identyfikator kontenera</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Brak wartości argumentu: „{}”</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Alias argumentu nie został rozpoznany dla bieżącego polecenia: „{}”</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Nieprawidłowy specyfikator argumentu: „{}”</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Nie znaleziono przylegającego aliasu flagi: „{}”</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>Przylegający alias nie jest flagą: „{}”</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Nieprawidłowy specyfikator argumentu: „{}”</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>Argument flagi nie może zawierać wartości przylegającej: „{}”</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Znaleziono argument pozycyjny, mimo że nie był on oczekiwany: „{}”</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Brak nazwy argumentu w: „{}”</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Nie można rozwiązać przekazanych argumentów zaczynających się od argumentu: „{}”</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Napotkano nieprawidłowy dodatkowy argument: „{}”</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Argumenty aliasu z wartością muszą znajdować się na końcu łańcucha aliasów: „{}”</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Copyright (c) Microsoft Corporation. Wszelkie prawa zastrzeżone.
2122 +Aby uzyskać informacje o prywatności dotyczące tego produktu, odwiedź stronę HTTP://aka.ms/privacy.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Nieprawidłowy obraz: „{}”</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Nieprawidłowa nazwa: „{}”</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Ścieżka nie jest bezwzględna: „{}”</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Nie znaleziono woluminu: „{}”</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Nieprawidłowe opcje woluminu: „{}”</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Nieobsługiwany typ woluminu: „{}”</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>Wolumin „{}” jest używany.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Znaleziono zarówno plik Dockerfile, jak i Containerfile. Użyj opcji -f, aby wybrać plik</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Nie można otworzyć „{}”: {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>Nie znaleziono pliku Containerfile ani Dockerfile w „{}”</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/pt-BR/Resources.resw
+568
@@ -1957,4 +1957,572 @@ Você também pode acessar mais opções do VS Code Remote através da paleta de
1957 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1958 <value>Integração com Visual Studio</value>
1959 </data>
1960 + <data name="MessageWslcUsage" xml:space="preserve">
1961 + <value>wslc - CLI de Contêiner WSL
1962 +Uso:
1963 + wslc --help</value>
1964 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1965 + </data>
1966 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1967 + <value>Sessão não encontrada: '{}'</value>
1968 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1969 + </data>
1970 + <data name="MessageInvalidIp" xml:space="preserve">
1971 + <value>Endereço de IP inválido “{}”</value>
1972 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1973 + </data>
1974 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1975 + <value>Falha em OpenSessionByName('{}')</value>
1976 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1979 + <value>Nenhuma sessão WSLC encontrada.</value>
1980 + </data>
1981 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1982 + <value>Encontrada {} sessão WSLC{}:</value>
1983 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1984 + </data>
1985 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1986 + <value>Falha no encerramento da sessão: "{}"</value>
1987 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1988 + </data>
1989 + <data name="MessageWslcShellExited" xml:space="preserve">
1990 + <value>{} saiu com: {}</value>
1991 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1992 + </data>
1993 + <data name="MessageWslcHeaderId" xml:space="preserve">
1994 + <value>Identificação</value>
1995 + </data>
1996 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1997 + <value>PID do Criador</value>
1998 + </data>
1999 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
2000 + <value>Nome de Exibição</value>
2001 + </data>
2002 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2003 + <value>Comando desconhecido: '{}'</value>
2004 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2005 + </data>
2006 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2007 + <value>Comando não reconhecido: '{}'</value>
2008 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2009 + </data>
2010 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2011 + <value>Argumento necessário não fornecido: '{}'</value>
2012 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2013 + </data>
2014 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2015 + <value>Argumento fornecido mais vezes do que o permitido: '{}'</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2019 + <value>Mostra a ajuda sobre o comando selecionado</value>
2020 + </data>
2021 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2022 + <value>Vários argumentos mutuamente exclusivos fornecidos: '{}'</value>
2023 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2024 + </data>
2025 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2026 + <value>O argumento {} só pode ser usado com {}</value>
2027 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2028 + </data>
2029 + <data name="WSLCCLI_Usage" xml:space="preserve">
2030 + <value>Uso: {} {}</value>
2031 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2032 + </data>
2033 + <data name="WSLCCLI_Command" xml:space="preserve">
2034 + <value>comando</value>
2035 + </data>
2036 + <data name="WSLCCLI_Options" xml:space="preserve">
2037 + <value>opções</value>
2038 + </data>
2039 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2040 + <value>Os seguintes nomes alternativos de comando estão disponíveis:</value>
2041 + </data>
2042 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2043 + <value>Os seguintes comandos estão disponíveis:</value>
2044 + </data>
2045 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2046 + <value>Os seguintes subcomandos estão disponíveis:</value>
2047 + </data>
2048 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2049 + <value>As seguintes opções estão disponíveis:</value>
2050 + </data>
2051 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2052 + <value>Os seguintes argumentos estão disponíveis:</value>
2053 + </data>
2054 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2055 + <value>Para obter mais detalhes sobre um comando específico, passe o argumento de ajuda.</value>
2056 + </data>
2057 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2058 + <value>O nome do argumento não foi reconhecido para o comando atual: '{}'</value>
2059 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2060 + </data>
2061 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2062 + <value>Este comando requer privilégios de administrador para ser executado.</value>
2063 + </data>
2064 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2065 + <value>Especificar a sessão a ser usada</value>
2066 + </data>
2067 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2068 + <value>Anexar ao stdout/stderr do contêiner</value>
2069 + </data>
2070 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2071 + <value>Anexar ao stdin e mantê-lo aberto</value>
2072 + </data>
2073 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2074 + <value>Especifique a porta a ser usada</value>
2075 + </data>
2076 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2077 + <value>ID do Contêiner</value>
2078 + </data>
2079 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2080 + <value>Valor do argumento ausente: '{}'</value>
2081 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2082 + </data>
2083 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2084 + <value>O alias de argumento não foi reconhecido para o comando atual: '{}'</value>
2085 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2086 + </data>
2087 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2088 + <value>Especificador de argumento inválido: '{}'</value>
2089 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2090 + </data>
2091 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2092 + <value>Alias de sinalizador adjacente não encontrado: '{}'</value>
2093 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2094 + </data>
2095 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2096 + <value>Alias adjacente não é um sinalizador: '{}'</value>
2097 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2098 + </data>
2099 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2100 + <value>Especificador de argumento inválido: '{}'</value>
2101 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2102 + </data>
2103 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2104 + <value>Argumento sinalizado não pode conter valores adjacentes: '{}'</value>
2105 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2106 + </data>
2107 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2108 + <value>Encontrado um argumento posicional quando nenhum era esperado: '{}'</value>
2109 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2110 + </data>
2111 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2112 + <value>Nome do argumento ausente em: '{}'</value>
2113 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2114 + </data>
2115 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2116 + <value>Falha ao resolve argumentos encaminhados iniciando no argumento: '{}'</value>
2117 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2118 + </data>
2119 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2120 + <value>Argumento extra inválido encontrado: '{}'</value>
2121 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2122 + </data>
2123 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2124 + <value>Os argumentos de alias com um valor devem ser os últimos na cadeia de alias: '{}'</value>
2125 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2126 + </data>
2127 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2128 + <value>Direitos autorais (c) Microsoft Corporation. Todos os direitos reservados.
2129 +Para obter informações sobre a privacidade desse produto, visite https://aka.ms/privacy.</value>
2130 + <comment>Copyright notice and privacy link</comment>
2131 + </data>
2132 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2133 + <value>Imagem inválida: '{}'</value>
2134 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2135 + </data>
2136 + <data name="MessageWslcInvalidName" xml:space="preserve">
2137 + <value>Nome inválido: '{}'</value>
2138 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2139 + </data>
2140 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2141 + <value>O caminho não é absoluto: '{}'</value>
2142 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2143 + </data>
2144 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2145 + <value>Volume não encontrado: '{}'</value>
2146 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2147 + </data>
2148 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2149 + <value>Opções de volume inválidas: '{}'.</value>
2150 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2151 + </data>
2152 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2153 + <value>Tipo de volume não suportado: '{}'.</value>
2154 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2155 + </data>
2156 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2157 + <value>O volume '{}' está em uso.</value>
2158 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2159 + </data>
2160 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2161 + <value>Dockerfile e Containerfile encontrados. Use -f para selecionar o arquivo a ser usado</value>
2162 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2163 + </data>
2164 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2165 + <value>Falha ao abrir "{}": {}</value>
2166 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2167 + </data>
2168 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2169 + <value>Nenhum Containerfile ou Dockerfile encontrado em "{}"</value>
2170 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2171 + </data>
2172 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2173 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2174 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2175 + </data>
2176 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2177 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2178 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2181 + <value>Manage containers.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2184 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2185 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2186 + </data>
2187 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2188 + <value>Attach to a container.</value>
2189 + </data>
2190 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2191 + <value>Attaches to a container.</value>
2192 + </data>
2193 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2194 + <value>Create a container.</value>
2195 + </data>
2196 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2197 + <value>Creates a container.</value>
2198 + </data>
2199 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2200 + <value>Execute a command in a running container.</value>
2201 + </data>
2202 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2203 + <value>Executes a command in a running container.</value>
2204 + </data>
2205 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2206 + <value>Inspect a container.</value>
2207 + </data>
2208 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2209 + <value>Display detailed information about a container.</value>
2210 + </data>
2211 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2212 + <value>Kill containers.</value>
2213 + </data>
2214 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2215 + <value>Kills containers.</value>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2218 + <value>List containers.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2221 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2222 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2223 + </data>
2224 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2225 + <value>View container logs.</value>
2226 + </data>
2227 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2228 + <value>View logs for a container.</value>
2229 + </data>
2230 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2231 + <value>Remove containers.</value>
2232 + </data>
2233 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2234 + <value>Removes containers.</value>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2237 + <value>Run a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2240 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2241 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2242 + </data>
2243 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2244 + <value>Start a container.</value>
2245 + </data>
2246 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2247 + <value>Starts a container.</value>
2248 + </data>
2249 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2250 + <value>Stop containers.</value>
2251 + </data>
2252 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2253 + <value>Stops containers.</value>
2254 + </data>
2255 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2256 + <value>Manage images.</value>
2257 + </data>
2258 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2259 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2260 + </data>
2261 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2262 + <value>Build an image from a Dockerfile.</value>
2263 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2264 + </data>
2265 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2266 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2267 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2268 + </data>
2269 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2270 + <value>Inspect images.</value>
2271 + </data>
2272 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2273 + <value>Inspect images.</value>
2274 + </data>
2275 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2276 + <value>List images.</value>
2277 + </data>
2278 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2279 + <value>Lists images.</value>
2280 + </data>
2281 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2282 + <value>Load images.</value>
2283 + </data>
2284 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2285 + <value>Loads images.</value>
2286 + </data>
2287 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2288 + <value>Pull images.</value>
2289 + </data>
2290 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2291 + <value>Pulls images.</value>
2292 + </data>
2293 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2294 + <value>Remove images.</value>
2295 + </data>
2296 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2297 + <value>Removes images.</value>
2298 + </data>
2299 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2300 + <value>Save images.</value>
2301 + </data>
2302 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2303 + <value>Saves images.</value>
2304 + </data>
2305 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2306 + <value>Manage sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2309 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2310 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2311 + </data>
2312 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2313 + <value>List sessions.</value>
2314 + </data>
2315 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2316 + <value>Lists active session(s).</value>
2317 + </data>
2318 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2319 + <value>Attach to a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2322 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2323 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2324 + </data>
2325 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2326 + <value>Terminate a session.</value>
2327 + </data>
2328 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2329 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2330 + </data>
2331 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2332 + <value>Open the settings file in the default editor.</value>
2333 + </data>
2334 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2335 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2336 +On first run, creates the file with all settings commented out at their defaults.</value>
2337 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2338 + </data>
2339 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2340 + <value>Reset settings to built-in defaults.</value>
2341 + </data>
2342 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2343 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2344 + </data>
2345 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2346 + <value>Settings reset to defaults.</value>
2347 + </data>
2348 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2349 + <value>Show all regardless of state.</value>
2350 + </data>
2351 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2352 + <value>Set build-time variables (KEY=VALUE)</value>
2353 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2354 + </data>
2355 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2356 + <value>The command to run</value>
2357 + </data>
2358 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2359 + <value>Delete containers even if they are running</value>
2360 + </data>
2361 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2362 + <value>Run container in detached mode</value>
2363 + </data>
2364 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2365 + <value>Specifies the container init process executable</value>
2366 + </data>
2367 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2368 + <value>Key=Value pairs for environment variables</value>
2369 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2370 + </data>
2371 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2372 + <value>File containing key=value pairs of env variables</value>
2373 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2374 + </data>
2375 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2376 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2377 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2380 + <value>Follow log output</value>
2381 + </data>
2382 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2383 + <value>Output formatting (json or table) (Default: table)</value>
2384 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2385 + </data>
2386 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2387 + <value>Arguments to pass to container's init process</value>
2388 + </data>
2389 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2390 + <value>Delete images even if they are being used</value>
2391 + </data>
2392 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2393 + <value>Image name</value>
2394 + </data>
2395 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2396 + <value>Provides path to the tar archive file containing the image</value>
2397 + </data>
2398 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2399 + <value>Name of the container</value>
2400 + </data>
2401 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2402 + <value>Do not delete untagged parents</value>
2403 + </data>
2404 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2405 + <value>Do not truncate output</value>
2406 + </data>
2407 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2408 + <value>Path for the saved image</value>
2409 + </data>
2410 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2411 + <value>Path to the build context directory</value>
2412 + </data>
2413 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2414 + <value>Publish a port from a container to host</value>
2415 + </data>
2416 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2417 + <value>Outputs the container IDs only</value>
2418 + </data>
2419 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2420 + <value>Remove the container after it stops</value>
2421 + </data>
2422 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2423 + <value>Session ID</value>
2424 + </data>
2425 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2426 + <value>Signal to send (default: {})</value>
2427 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2428 + </data>
2429 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2430 + <value>Tag for the built image</value>
2431 + </data>
2432 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2433 + <value>Time in seconds to wait before executing (default 5)</value>
2434 + </data>
2435 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2436 + <value>Open a TTY with the container process.</value>
2437 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2438 + </data>
2439 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2440 + <value>Output verbose details</value>
2441 + </data>
2442 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2443 + <value>Show version information for this tool</value>
2444 + </data>
2445 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2446 + <value>Bind mount a volume to the container</value>
2447 + </data>
2448 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2449 + <value>Write the container ID to the provided path.</value>
2450 + </data>
2451 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2452 + <value>IP address of the DNS nameserver in resolv.conf</value>
2453 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2454 + </data>
2455 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2456 + <value>Set the default DNS Domain</value>
2457 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2458 + </data>
2459 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2460 + <value>Set DNS options</value>
2461 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2462 + </data>
2463 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2464 + <value>Set DNS search domains</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2468 + <value>Group Id for the process</value>
2469 + </data>
2470 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2471 + <value>No configuration of DNS in the container</value>
2472 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2473 + </data>
2474 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2475 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2476 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2477 + </data>
2478 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2479 + <value>Image pull policy (always|missing|never) (default:never)</value>
2480 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2483 + <value>Use this scheme for registry connection</value>
2484 + </data>
2485 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2486 + <value>Mount tmpfs to the container at the given path</value>
2487 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2488 + </data>
2489 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2490 + <value>User ID for the process (name|uid|uid:gid)</value>
2491 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2492 + </data>
2493 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2494 + <value>Expose virtualization capabilities to the container</value>
2495 + </data>
2496 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2497 + <value>Arguments to pass to the command being executed inside the container</value>
2498 + </data>
2499 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2500 + <value>Show detailed information about the listed sessions.</value>
2501 + </data>
2502 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2503 + <value>Invalid format type specified. Supported format types are: json, table</value>
2504 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2505 + </data>
2506 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2507 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2508 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2509 + </data>
2510 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2511 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2512 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2513 + </data>
2514 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2515 + <value>Image '{}' not found, pulling</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2519 + <value>Environment variable key cannot be empty</value>
2520 + </data>
2521 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2522 + <value>Environment variable key '{}' cannot contain whitespace</value>
2523 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2524 + </data>
2525 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2526 + <value>Requested load but no input provided.</value>
2527 + </data>
2528 </root>
\ No newline at end of file
localization/strings/pt-PT/Resources.resw
+568
@@ -1950,4 +1950,572 @@ Também pode aceder a mais Opções remotas do VS Code através da paleta de com
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Integração do Visual Studio</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc - CLI de Contentor WSL
1955 +Utilização:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Sessão não encontrada: '{}'</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Endereço IP inválido "{}"</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName('{}') falhou</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>Não foram encontradas sessões WSLC.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>Encontrada {} sessão WSLC{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Falha ao terminar a sessão: "{}"</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} terminou com: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>ID</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>PID do Criador</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Nome a Apresentar</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Comando desconhecido: '{}'</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Comando não reconhecido: '{}'</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Argumento necessário não fornecido: '{}'</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>O argumento foi fornecido mais vezes do que o permitido: '{}'</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Mostra ajuda sobre o comando selecionado</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Foram fornecidos vários argumentos mutuamente exclusivos: '{}'</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>O argumento {} só pode ser utilizado com {}</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Utilização: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>comando</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>opções</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>Estão disponíveis os seguintes aliases de comando:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>Estão disponíveis os seguintes comandos:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>Estão disponíveis os seguintes subcomandos:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>Estão disponíveis as seguintes opções:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>Estão disponíveis os seguintes argumentos:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Para obter mais detalhes sobre um comando específico, transmita o argumento de ajuda para o mesmo.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>O nome do argumento não foi reconhecido para o comando atual: '{}'</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>Este comando requer privilégios de administrador para ser executado.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Especificar a sessão a utilizar</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Anexar a stdout/stderr do contentor</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Anexar ao stdin e mantê-lo aberto</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Especificar a porta a utilizar</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>ID do Contentor</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Valor de argumento em falta: '{}'</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>O alias do argumento não foi reconhecido para o comando atual: '{}'</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Especificador de argumento inválido: '{}'</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Alias de sinalizador adjacente não encontrado: '{}'</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>O alias adjacente não é um sinalizador: '{}'</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Especificador de argumento inválido: '{}'</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>O argumento de sinalizador não pode conter um valor adjacente: '{}'</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Foi encontrado um argumento posicional quando não era esperado nenhum: '{}'</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Nome do argumento em falta em: '{}'</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Falha ao resolver os argumentos reencaminhados a partir do argumento: "{}"</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Foi encontrado um argumento adicional inválido: '{}'</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Os argumentos de alias com um valor têm de ser os últimos na cadeia de alias: '{}'</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Copyright (c) Microsoft Corporation. Todos os direitos reservados.
2122 +Para obter informações de privacidade sobre este produto, visite https://aka.ms/privacy.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Imagem inválida: "{}"</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Nome inválido: '{}'</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>O caminho não é absoluto: "{}"</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Volume não encontrado: '{}'</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Opções de volume inválidas: '{}'</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Tipo de volume não suportado: '{}'</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>O volume '{}' está em utilização.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Dockerfile e Containerfile encontrados. Utilize -f para selecionar o ficheiro a utilizar</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Falha ao abrir "{}": {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>Não foi encontrado Containerfile ou Dockerfile em "{}"</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/ru-RU/Resources.resw
+568
@@ -1957,4 +1957,572 @@ wsl.exe --manage &lt;DistributionName&gt; --set-sparse true --allow-unsafe</valu
1957 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1958 <value>Интеграция Visual Studio</value>
1959 </data>
1960 + <data name="MessageWslcUsage" xml:space="preserve">
1961 + <value>wslc — CLI контейнера WSL
1962 +Использование:
1963 + wslc --help</value>
1964 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1965 + </data>
1966 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1967 + <value>Сеанс не найден: "{}"</value>
1968 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1969 + </data>
1970 + <data name="MessageInvalidIp" xml:space="preserve">
1971 + <value>Недопустимый IP-адрес "{}"</value>
1972 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1973 + </data>
1974 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1975 + <value>Сбой OpenSessionByName("{}")</value>
1976 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1979 + <value>Сеансы WSLC не найдены.</value>
1980 + </data>
1981 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1982 + <value>Найден {} сеанс WSLC{}:</value>
1983 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1984 + </data>
1985 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1986 + <value>Сбой завершения сеанса: "{}"</value>
1987 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1988 + </data>
1989 + <data name="MessageWslcShellExited" xml:space="preserve">
1990 + <value>{} выходит с: {}</value>
1991 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1992 + </data>
1993 + <data name="MessageWslcHeaderId" xml:space="preserve">
1994 + <value>Код</value>
1995 + </data>
1996 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1997 + <value>ИД процесса автора</value>
1998 + </data>
1999 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
2000 + <value>Выводимое имя</value>
2001 + </data>
2002 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2003 + <value>Неизвестная команда: "{}"</value>
2004 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2005 + </data>
2006 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2007 + <value>Нераспознанная команда: "{}"</value>
2008 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2009 + </data>
2010 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2011 + <value>Обязательный аргумент не указан: "{}"</value>
2012 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2013 + </data>
2014 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2015 + <value>Аргумент предоставлен больше раз, чем разрешено: "{}"</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2019 + <value>Отображает справку по выбранной команде</value>
2020 + </data>
2021 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2022 + <value>Указано несколько взаимоисключающих аргументов: {}</value>
2023 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2024 + </data>
2025 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2026 + <value>Аргумент {} можно использовать только с {}</value>
2027 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2028 + </data>
2029 + <data name="WSLCCLI_Usage" xml:space="preserve">
2030 + <value>Использование: {} {}</value>
2031 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2032 + </data>
2033 + <data name="WSLCCLI_Command" xml:space="preserve">
2034 + <value>команда</value>
2035 + </data>
2036 + <data name="WSLCCLI_Options" xml:space="preserve">
2037 + <value>параметры</value>
2038 + </data>
2039 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2040 + <value>Доступны следующие псевдонимы команд.</value>
2041 + </data>
2042 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2043 + <value>Применимы следующие команды:</value>
2044 + </data>
2045 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2046 + <value>Доступны следующие подкоманды:</value>
2047 + </data>
2048 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2049 + <value>Доступны следующие опции:</value>
2050 + </data>
2051 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2052 + <value>Доступны следующие аргументы:</value>
2053 + </data>
2054 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2055 + <value>Для более подробной информации о конкретной команде передайте ей аргумент справки.</value>
2056 + </data>
2057 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2058 + <value>Имя аргумента не распознано для текущей команды: "{}"</value>
2059 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2060 + </data>
2061 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2062 + <value>Для выполнения этой команды требуются права администратора.</value>
2063 + </data>
2064 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2065 + <value>Укажите сеанс для использования</value>
2066 + </data>
2067 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2068 + <value>Присоединение к stdout/stderr контейнера</value>
2069 + </data>
2070 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2071 + <value>Подключиться к stdin и оставить его открытым</value>
2072 + </data>
2073 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2074 + <value>Укажите порт для использования</value>
2075 + </data>
2076 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2077 + <value>ИД контейнера</value>
2078 + </data>
2079 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2080 + <value>Отсутствует значение аргумента: "{}"</value>
2081 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2082 + </data>
2083 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2084 + <value>Псевдоним аргумента не был распознан для текущей команды: "{}"</value>
2085 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2086 + </data>
2087 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2088 + <value>Неверный указатель аргумента: "{}"</value>
2089 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2090 + </data>
2091 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2092 + <value>Псевдоним присоединенного флага не найден: "{}"</value>
2093 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2094 + </data>
2095 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2096 + <value>Присоединенный псевдоним не является флагом: "{}"</value>
2097 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2098 + </data>
2099 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2100 + <value>Неверный указатель аргумента: "{}"</value>
2101 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2102 + </data>
2103 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2104 + <value>Аргумент флага не может содержать присоединенное значение: "{}"</value>
2105 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2106 + </data>
2107 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2108 + <value>Найден позиционный аргумент, когда не ожидалось: "{}"</value>
2109 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2110 + </data>
2111 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2112 + <value>Отсутствует имя аргумента в: "{}"</value>
2113 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2114 + </data>
2115 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2116 + <value>Не удалось разрешить переданные аргументы, начиная с аргумента: '{}'</value>
2117 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2118 + </data>
2119 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2120 + <value>Обнаружен недопустимый дополнительный аргумент: "{}"</value>
2121 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2122 + </data>
2123 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2124 + <value>Аргументы псевдонима со значением должны быть последними в цепочке псевдонимов: "{}"</value>
2125 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2126 + </data>
2127 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2128 + <value>(c) Корпорация Майкрософт (Microsoft Corporation). Все права защищены.
2129 +Сведения о конфиденциальности этого продукта см. на странице https://aka.ms/privacy.</value>
2130 + <comment>Copyright notice and privacy link</comment>
2131 + </data>
2132 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2133 + <value>Недопустимое изображение: "{}"</value>
2134 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2135 + </data>
2136 + <data name="MessageWslcInvalidName" xml:space="preserve">
2137 + <value>Недопустимое имя: "{}"</value>
2138 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2139 + </data>
2140 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2141 + <value>Путь не является абсолютным: "{}"</value>
2142 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2143 + </data>
2144 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2145 + <value>Том не найден: "{}"</value>
2146 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2147 + </data>
2148 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2149 + <value>Недопустимые параметры тома: "{}"</value>
2150 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2151 + </data>
2152 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2153 + <value>Неподдерживаемый тип тома: "{}"</value>
2154 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2155 + </data>
2156 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2157 + <value>Том "{}" используется.</value>
2158 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2159 + </data>
2160 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2161 + <value>Найдены оба файла: Dockerfile и Containerfile. Используйте -f для выбора файла</value>
2162 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2163 + </data>
2164 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2165 + <value>Не удалось открыть "{}": {}</value>
2166 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2167 + </data>
2168 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2169 + <value>Containerfile или Dockerfile не найдены в '{}'.</value>
2170 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2171 + </data>
2172 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2173 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2174 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2175 + </data>
2176 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2177 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2178 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2181 + <value>Manage containers.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2184 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2185 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2186 + </data>
2187 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2188 + <value>Attach to a container.</value>
2189 + </data>
2190 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2191 + <value>Attaches to a container.</value>
2192 + </data>
2193 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2194 + <value>Create a container.</value>
2195 + </data>
2196 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2197 + <value>Creates a container.</value>
2198 + </data>
2199 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2200 + <value>Execute a command in a running container.</value>
2201 + </data>
2202 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2203 + <value>Executes a command in a running container.</value>
2204 + </data>
2205 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2206 + <value>Inspect a container.</value>
2207 + </data>
2208 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2209 + <value>Display detailed information about a container.</value>
2210 + </data>
2211 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2212 + <value>Kill containers.</value>
2213 + </data>
2214 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2215 + <value>Kills containers.</value>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2218 + <value>List containers.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2221 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2222 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2223 + </data>
2224 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2225 + <value>View container logs.</value>
2226 + </data>
2227 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2228 + <value>View logs for a container.</value>
2229 + </data>
2230 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2231 + <value>Remove containers.</value>
2232 + </data>
2233 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2234 + <value>Removes containers.</value>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2237 + <value>Run a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2240 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2241 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2242 + </data>
2243 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2244 + <value>Start a container.</value>
2245 + </data>
2246 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2247 + <value>Starts a container.</value>
2248 + </data>
2249 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2250 + <value>Stop containers.</value>
2251 + </data>
2252 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2253 + <value>Stops containers.</value>
2254 + </data>
2255 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2256 + <value>Manage images.</value>
2257 + </data>
2258 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2259 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2260 + </data>
2261 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2262 + <value>Build an image from a Dockerfile.</value>
2263 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2264 + </data>
2265 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2266 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2267 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2268 + </data>
2269 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2270 + <value>Inspect images.</value>
2271 + </data>
2272 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2273 + <value>Inspect images.</value>
2274 + </data>
2275 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2276 + <value>List images.</value>
2277 + </data>
2278 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2279 + <value>Lists images.</value>
2280 + </data>
2281 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2282 + <value>Load images.</value>
2283 + </data>
2284 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2285 + <value>Loads images.</value>
2286 + </data>
2287 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2288 + <value>Pull images.</value>
2289 + </data>
2290 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2291 + <value>Pulls images.</value>
2292 + </data>
2293 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2294 + <value>Remove images.</value>
2295 + </data>
2296 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2297 + <value>Removes images.</value>
2298 + </data>
2299 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2300 + <value>Save images.</value>
2301 + </data>
2302 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2303 + <value>Saves images.</value>
2304 + </data>
2305 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2306 + <value>Manage sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2309 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2310 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2311 + </data>
2312 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2313 + <value>List sessions.</value>
2314 + </data>
2315 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2316 + <value>Lists active session(s).</value>
2317 + </data>
2318 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2319 + <value>Attach to a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2322 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2323 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2324 + </data>
2325 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2326 + <value>Terminate a session.</value>
2327 + </data>
2328 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2329 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2330 + </data>
2331 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2332 + <value>Open the settings file in the default editor.</value>
2333 + </data>
2334 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2335 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2336 +On first run, creates the file with all settings commented out at their defaults.</value>
2337 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2338 + </data>
2339 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2340 + <value>Reset settings to built-in defaults.</value>
2341 + </data>
2342 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2343 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2344 + </data>
2345 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2346 + <value>Settings reset to defaults.</value>
2347 + </data>
2348 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2349 + <value>Show all regardless of state.</value>
2350 + </data>
2351 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2352 + <value>Set build-time variables (KEY=VALUE)</value>
2353 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2354 + </data>
2355 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2356 + <value>The command to run</value>
2357 + </data>
2358 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2359 + <value>Delete containers even if they are running</value>
2360 + </data>
2361 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2362 + <value>Run container in detached mode</value>
2363 + </data>
2364 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2365 + <value>Specifies the container init process executable</value>
2366 + </data>
2367 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2368 + <value>Key=Value pairs for environment variables</value>
2369 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2370 + </data>
2371 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2372 + <value>File containing key=value pairs of env variables</value>
2373 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2374 + </data>
2375 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2376 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2377 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2380 + <value>Follow log output</value>
2381 + </data>
2382 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2383 + <value>Output formatting (json or table) (Default: table)</value>
2384 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2385 + </data>
2386 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2387 + <value>Arguments to pass to container's init process</value>
2388 + </data>
2389 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2390 + <value>Delete images even if they are being used</value>
2391 + </data>
2392 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2393 + <value>Image name</value>
2394 + </data>
2395 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2396 + <value>Provides path to the tar archive file containing the image</value>
2397 + </data>
2398 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2399 + <value>Name of the container</value>
2400 + </data>
2401 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2402 + <value>Do not delete untagged parents</value>
2403 + </data>
2404 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2405 + <value>Do not truncate output</value>
2406 + </data>
2407 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2408 + <value>Path for the saved image</value>
2409 + </data>
2410 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2411 + <value>Path to the build context directory</value>
2412 + </data>
2413 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2414 + <value>Publish a port from a container to host</value>
2415 + </data>
2416 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2417 + <value>Outputs the container IDs only</value>
2418 + </data>
2419 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2420 + <value>Remove the container after it stops</value>
2421 + </data>
2422 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2423 + <value>Session ID</value>
2424 + </data>
2425 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2426 + <value>Signal to send (default: {})</value>
2427 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2428 + </data>
2429 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2430 + <value>Tag for the built image</value>
2431 + </data>
2432 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2433 + <value>Time in seconds to wait before executing (default 5)</value>
2434 + </data>
2435 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2436 + <value>Open a TTY with the container process.</value>
2437 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2438 + </data>
2439 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2440 + <value>Output verbose details</value>
2441 + </data>
2442 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2443 + <value>Show version information for this tool</value>
2444 + </data>
2445 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2446 + <value>Bind mount a volume to the container</value>
2447 + </data>
2448 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2449 + <value>Write the container ID to the provided path.</value>
2450 + </data>
2451 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2452 + <value>IP address of the DNS nameserver in resolv.conf</value>
2453 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2454 + </data>
2455 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2456 + <value>Set the default DNS Domain</value>
2457 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2458 + </data>
2459 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2460 + <value>Set DNS options</value>
2461 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2462 + </data>
2463 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2464 + <value>Set DNS search domains</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2468 + <value>Group Id for the process</value>
2469 + </data>
2470 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2471 + <value>No configuration of DNS in the container</value>
2472 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2473 + </data>
2474 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2475 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2476 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2477 + </data>
2478 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2479 + <value>Image pull policy (always|missing|never) (default:never)</value>
2480 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2483 + <value>Use this scheme for registry connection</value>
2484 + </data>
2485 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2486 + <value>Mount tmpfs to the container at the given path</value>
2487 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2488 + </data>
2489 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2490 + <value>User ID for the process (name|uid|uid:gid)</value>
2491 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2492 + </data>
2493 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2494 + <value>Expose virtualization capabilities to the container</value>
2495 + </data>
2496 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2497 + <value>Arguments to pass to the command being executed inside the container</value>
2498 + </data>
2499 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2500 + <value>Show detailed information about the listed sessions.</value>
2501 + </data>
2502 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2503 + <value>Invalid format type specified. Supported format types are: json, table</value>
2504 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2505 + </data>
2506 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2507 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2508 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2509 + </data>
2510 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2511 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2512 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2513 + </data>
2514 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2515 + <value>Image '{}' not found, pulling</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2519 + <value>Environment variable key cannot be empty</value>
2520 + </data>
2521 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2522 + <value>Environment variable key '{}' cannot contain whitespace</value>
2523 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2524 + </data>
2525 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2526 + <value>Requested load but no input provided.</value>
2527 + </data>
2528 </root>
\ No newline at end of file
localization/strings/sv-SE/Resources.resw
+568
@@ -1950,4 +1950,572 @@ Du kan också komma åt fler VS Code-fjärralternativ via kommandopaletten inom
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Visual Studio integrering</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc – WSL-container CLI
1955 +Användning:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Sessionen kunde inte hittas: '{}'</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Ogiltig IP-adress: {}</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName('{}') misslyckades</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>Inga WSLC-sessioner hittades.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>Hittade {} WSLC-session{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Det gick inte att avsluta sessionen: '{}'</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} avslutades med: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>ID</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>Skapar-PID</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Visningsnamn</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Okänt kommando '{}'</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Okänt kommando: "{}"</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Obligatoriska argument saknas: "{}"</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Argumentet tillhandahölls fler gånger än tillåtet: "{}"</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Visar hjälp om det markerade kommandot</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Flera ömsesidigt uteslutande argument har angetts: {}</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>Argumentet {} kan bara användas med {}</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Användning: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>kommando</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>alternativ</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>Följande kommandoalias är tillgängliga:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>Följande kommandon är tillgängliga:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>Följande underkommandon är tillgängliga:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>Följande alternativ är tillgängliga:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>Följande argument är tillgängliga:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Om du vill ha mer information om ett speciellt kommando kan du skicka den till hjälpargumentet.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Argumentnamnet kändes inte igen för det aktuella kommandot: "{}"</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>Det krävs administratörsrättigheter för att köra detta kommando.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Ange vilken session som ska användas</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Bifoga till stdout/stderr för containern</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Anslut till stdin och håll den öppen</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Ange den port som ska användas</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>Behållar-ID</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Argumentvärde saknas: "{}"</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Argumentalias kändes inte igen för det aktuella kommandot: "{}"</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Ogiltig argumentspecifierare: "{}"</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Angränsande flaggalias hittades inte: "{}"</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>Angränsande alias är inte en flagga: "{}"</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Ogiltig argumentspecifierare: "{}"</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>Flaggargumentet kan inte innehålla ett angränsande värde: "{}"</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Hittade ett positionsargument när inget förväntades: "{}"</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Argumentnamnet saknas vid: "{}"</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>Det gick inte att matcha vidarebefordrade argument med början vid argumentet: "{}"</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Ett ogiltigt extra argument påträffades: "{}"</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Aliasargument med ett värde måste vara sist i aliaskedjan: "{}"</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Copyright (c) Microsoft Corporation. Med ensamrätt.
2122 +För sekretessinformation om den här produkten, besök https://aka.ms/privacy.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Ogiltig bild ''{}''</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Ogiltigt namn: {}</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Sökvägen är inte absolut: ''{}''</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Volymen hittades inte: {}</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Ogiltiga volymalternativ: {}</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Volymtypen stöds inte: {}</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>Volymen {} används.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Både Dockerfile och Containerfile hittas. Använd -f för att välja vilken fil som ska användas</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>Det gick inte att öppna '{}': {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>Ingen Containerfile eller Dockerfile hittades i '{}'</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/tr-TR/Resources.resw
+568
@@ -1950,4 +1950,572 @@ Ayrıca VS Code'un içindeki komut paleti aracılığıyla daha fazla VS Code Re
1950 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1951 <value>Visual Studio Tümleştirmesi</value>
1952 </data>
1953 + <data name="MessageWslcUsage" xml:space="preserve">
1954 + <value>wslc - WSL Kapsayıcı CLI’si
1955 +Kullanım:
1956 + wslc --help</value>
1957 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1958 + </data>
1959 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1960 + <value>Oturum bulunamadı: '{}'</value>
1961 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1962 + </data>
1963 + <data name="MessageInvalidIp" xml:space="preserve">
1964 + <value>Geçersiz IP adresi '{}'</value>
1965 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1966 + </data>
1967 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1968 + <value>OpenSessionByName('{}') başarısız oldu</value>
1969 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1970 + </data>
1971 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1972 + <value>WSLA oturumu bulunamadı.</value>
1973 + </data>
1974 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1975 + <value>{} WSLC oturumu bulundu{}:</value>
1976 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1977 + </data>
1978 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1979 + <value>Oturum sonlandırılamadı: '{}'</value>
1980 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1981 + </data>
1982 + <data name="MessageWslcShellExited" xml:space="preserve">
1983 + <value>{} şu kodla çıktı: {}</value>
1984 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1985 + </data>
1986 + <data name="MessageWslcHeaderId" xml:space="preserve">
1987 + <value>Kimlik</value>
1988 + </data>
1989 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1990 + <value>Oluşturan PID</value>
1991 + </data>
1992 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1993 + <value>Görünen Ad</value>
1994 + </data>
1995 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
1996 + <value>Bilinmeyen komut: '{}'</value>
1997 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1998 + </data>
1999 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2000 + <value>Tanınmayan komut: '{}'</value>
2001 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2002 + </data>
2003 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2004 + <value>Gerekli bağımsız değişken sağlanmadı: '{}'</value>
2005 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2006 + </data>
2007 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2008 + <value>Bağımsız değişken, izin verilenden daha fazla kez sağlandı: '{}'</value>
2009 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2010 + </data>
2011 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2012 + <value>Seçili komut hakkında yardım görüntüler</value>
2013 + </data>
2014 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2015 + <value>Birbirini dışlayan birden çok bağımsız değişken sağlandı: '{}'</value>
2016 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2017 + </data>
2018 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2019 + <value>{} bağımsız değişkeni yalnızca {} ile kullanılabilir</value>
2020 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2021 + </data>
2022 + <data name="WSLCCLI_Usage" xml:space="preserve">
2023 + <value>Kullanım: {} {}</value>
2024 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2025 + </data>
2026 + <data name="WSLCCLI_Command" xml:space="preserve">
2027 + <value>komut</value>
2028 + </data>
2029 + <data name="WSLCCLI_Options" xml:space="preserve">
2030 + <value>seçenekler</value>
2031 + </data>
2032 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2033 + <value>Aşağıdaki komut diğer adları kullanılabilir:</value>
2034 + </data>
2035 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2036 + <value>Şu komutlar kullanılabilir:</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2039 + <value>Şu alt komutlar kullanılabilir:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2042 + <value>Şu seçenekler kullanılabilir:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2045 + <value>Şu bağımsız değişkenler kullanılabilir:</value>
2046 + </data>
2047 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2048 + <value>Belirli bir komut hakkında daha fazla ayrıntı için, ilgili komutu yardım bağımsız değişkenine geçirin.</value>
2049 + </data>
2050 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2051 + <value>Bağımsız değişken adı geçerli komut için tanınmadı: '{}'</value>
2052 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2053 + </data>
2054 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2055 + <value>Bu komutun yürütülebilmesi için yönetici ayrıcalıkları gerekir.</value>
2056 + </data>
2057 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2058 + <value>Kullanılacak oturumu belirtin</value>
2059 + </data>
2060 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2061 + <value>Kapsayıcının stdout’una/standart hatasına ekle</value>
2062 + </data>
2063 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2064 + <value>Standart girdiye ekle ve açık tut</value>
2065 + </data>
2066 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2067 + <value>Kullanılacak bağlantı noktasını belirtin</value>
2068 + </data>
2069 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2070 + <value>Kapsayıcı kimliği</value>
2071 + </data>
2072 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2073 + <value>Eksik bağımsız değişken değeri: '{}'</value>
2074 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2075 + </data>
2076 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2077 + <value>Bağımsız değişken diğer adı geçerli komut için tanınmadı: '{}'</value>
2078 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2079 + </data>
2080 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2081 + <value>Geçersiz bağımsız değişken belirticisi: '{}'</value>
2082 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2083 + </data>
2084 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2085 + <value>Eklenmiş bayrak diğer adı bulunamadı: '{}'</value>
2086 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2087 + </data>
2088 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2089 + <value>Ekli diğer ad bir bayrak değil: '{}'</value>
2090 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2091 + </data>
2092 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2093 + <value>Geçersiz bağımsız değişken belirticisi: '{}'</value>
2094 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2095 + </data>
2096 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2097 + <value>Bayrak bağımsız değişkeni ekli değeri içeremez: '{}'</value>
2098 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2099 + </data>
2100 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2101 + <value>Beklenmediği halde bir konumsal bağımsız değişken bulundu: '{}'</value>
2102 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2103 + </data>
2104 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2105 + <value>Şurada eksik bağımsız değişken adı: '{}'</value>
2106 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2107 + </data>
2108 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2109 + <value>İletilen bağımsız değişkenler şu bağımsız değişkenden başlayarak çözümlenemedi: '{}'</value>
2110 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2111 + </data>
2112 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2113 + <value>Geçersiz ek bağımsız değişkenle karşılaşıldı: '{}'</value>
2114 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2115 + </data>
2116 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2117 + <value>Değere sahip diğer ad bağımsız değişkenleri diğer ad zincirinde son olmalıdır: '{}'</value>
2118 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2119 + </data>
2120 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2121 + <value>Telif Hakkı (c) Microsoft Corporation. Tüm hakları saklıdır.
2122 +Bu ürünle ilgili gizlilik bilgileri için lütfen https://aka.ms/privacy sayfasını ziyaret edin.</value>
2123 + <comment>Copyright notice and privacy link</comment>
2124 + </data>
2125 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2126 + <value>Geçersiz resim: '{}'</value>
2127 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2128 + </data>
2129 + <data name="MessageWslcInvalidName" xml:space="preserve">
2130 + <value>Geçersiz ad: '{}'</value>
2131 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2132 + </data>
2133 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2134 + <value>Yol mutlak değil: '{}'</value>
2135 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 + </data>
2137 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2138 + <value>Birim bulunamadı: '{}'</value>
2139 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2140 + </data>
2141 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2142 + <value>Geçersiz birim seçenekleri: '{}'</value>
2143 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2144 + </data>
2145 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2146 + <value>Desteklenmeyen birim türü: '{}'</value>
2147 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2148 + </data>
2149 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2150 + <value>'{}' birimi kullanımda.</value>
2151 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2152 + </data>
2153 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2154 + <value>Hem Dockerfile hem de Containerfile bulundu. Kullanılacak dosyayı seçmek için -f kullanın</value>
2155 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2156 + </data>
2157 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2158 + <value>'{}' açılamadı: {}</value>
2159 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2160 + </data>
2161 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2162 + <value>'{}' içinde Containerfile veya Dockerfile bulunamadı</value>
2163 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2164 + </data>
2165 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2166 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2167 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2168 + </data>
2169 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2170 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2171 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2172 + </data>
2173 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2174 + <value>Manage containers.</value>
2175 + </data>
2176 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2177 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2178 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2179 + </data>
2180 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2181 + <value>Attach to a container.</value>
2182 + </data>
2183 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2184 + <value>Attaches to a container.</value>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2187 + <value>Create a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2190 + <value>Creates a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2193 + <value>Execute a command in a running container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2196 + <value>Executes a command in a running container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2199 + <value>Inspect a container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2202 + <value>Display detailed information about a container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2205 + <value>Kill containers.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2208 + <value>Kills containers.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2211 + <value>List containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2214 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2215 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2216 + </data>
2217 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2218 + <value>View container logs.</value>
2219 + </data>
2220 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2221 + <value>View logs for a container.</value>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2224 + <value>Remove containers.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2227 + <value>Removes containers.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2230 + <value>Run a container.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2233 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2234 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2235 + </data>
2236 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2237 + <value>Start a container.</value>
2238 + </data>
2239 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2240 + <value>Starts a container.</value>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2243 + <value>Stop containers.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2246 + <value>Stops containers.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2249 + <value>Manage images.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2252 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2255 + <value>Build an image from a Dockerfile.</value>
2256 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2257 + </data>
2258 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2259 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2260 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2261 + </data>
2262 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2263 + <value>Inspect images.</value>
2264 + </data>
2265 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2266 + <value>Inspect images.</value>
2267 + </data>
2268 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2269 + <value>List images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2272 + <value>Lists images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2275 + <value>Load images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2278 + <value>Loads images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2281 + <value>Pull images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2284 + <value>Pulls images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2287 + <value>Remove images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2290 + <value>Removes images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2293 + <value>Save images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2296 + <value>Saves images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2299 + <value>Manage sessions.</value>
2300 + </data>
2301 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2302 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2303 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2304 + </data>
2305 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2306 + <value>List sessions.</value>
2307 + </data>
2308 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2309 + <value>Lists active session(s).</value>
2310 + </data>
2311 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2312 + <value>Attach to a session.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2315 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2316 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2317 + </data>
2318 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2319 + <value>Terminate a session.</value>
2320 + </data>
2321 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2322 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2323 + </data>
2324 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2325 + <value>Open the settings file in the default editor.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2328 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2329 +On first run, creates the file with all settings commented out at their defaults.</value>
2330 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2331 + </data>
2332 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2333 + <value>Reset settings to built-in defaults.</value>
2334 + </data>
2335 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2336 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2339 + <value>Settings reset to defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2342 + <value>Show all regardless of state.</value>
2343 + </data>
2344 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2345 + <value>Set build-time variables (KEY=VALUE)</value>
2346 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2347 + </data>
2348 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2349 + <value>The command to run</value>
2350 + </data>
2351 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2352 + <value>Delete containers even if they are running</value>
2353 + </data>
2354 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2355 + <value>Run container in detached mode</value>
2356 + </data>
2357 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2358 + <value>Specifies the container init process executable</value>
2359 + </data>
2360 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2361 + <value>Key=Value pairs for environment variables</value>
2362 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2363 + </data>
2364 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2365 + <value>File containing key=value pairs of env variables</value>
2366 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2367 + </data>
2368 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2369 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2370 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2371 + </data>
2372 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2373 + <value>Follow log output</value>
2374 + </data>
2375 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2376 + <value>Output formatting (json or table) (Default: table)</value>
2377 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2378 + </data>
2379 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2380 + <value>Arguments to pass to container's init process</value>
2381 + </data>
2382 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2383 + <value>Delete images even if they are being used</value>
2384 + </data>
2385 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2386 + <value>Image name</value>
2387 + </data>
2388 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2389 + <value>Provides path to the tar archive file containing the image</value>
2390 + </data>
2391 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2392 + <value>Name of the container</value>
2393 + </data>
2394 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2395 + <value>Do not delete untagged parents</value>
2396 + </data>
2397 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2398 + <value>Do not truncate output</value>
2399 + </data>
2400 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2401 + <value>Path for the saved image</value>
2402 + </data>
2403 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2404 + <value>Path to the build context directory</value>
2405 + </data>
2406 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2407 + <value>Publish a port from a container to host</value>
2408 + </data>
2409 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2410 + <value>Outputs the container IDs only</value>
2411 + </data>
2412 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2413 + <value>Remove the container after it stops</value>
2414 + </data>
2415 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2416 + <value>Session ID</value>
2417 + </data>
2418 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2419 + <value>Signal to send (default: {})</value>
2420 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2421 + </data>
2422 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2423 + <value>Tag for the built image</value>
2424 + </data>
2425 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2426 + <value>Time in seconds to wait before executing (default 5)</value>
2427 + </data>
2428 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2429 + <value>Open a TTY with the container process.</value>
2430 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2431 + </data>
2432 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2433 + <value>Output verbose details</value>
2434 + </data>
2435 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2436 + <value>Show version information for this tool</value>
2437 + </data>
2438 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2439 + <value>Bind mount a volume to the container</value>
2440 + </data>
2441 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2442 + <value>Write the container ID to the provided path.</value>
2443 + </data>
2444 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2445 + <value>IP address of the DNS nameserver in resolv.conf</value>
2446 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2447 + </data>
2448 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2449 + <value>Set the default DNS Domain</value>
2450 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2451 + </data>
2452 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2453 + <value>Set DNS options</value>
2454 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2455 + </data>
2456 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2457 + <value>Set DNS search domains</value>
2458 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2459 + </data>
2460 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2461 + <value>Group Id for the process</value>
2462 + </data>
2463 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2464 + <value>No configuration of DNS in the container</value>
2465 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2466 + </data>
2467 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2468 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2469 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2470 + </data>
2471 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2472 + <value>Image pull policy (always|missing|never) (default:never)</value>
2473 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2474 + </data>
2475 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2476 + <value>Use this scheme for registry connection</value>
2477 + </data>
2478 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2479 + <value>Mount tmpfs to the container at the given path</value>
2480 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2481 + </data>
2482 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2483 + <value>User ID for the process (name|uid|uid:gid)</value>
2484 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2485 + </data>
2486 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2487 + <value>Expose virtualization capabilities to the container</value>
2488 + </data>
2489 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2490 + <value>Arguments to pass to the command being executed inside the container</value>
2491 + </data>
2492 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2493 + <value>Show detailed information about the listed sessions.</value>
2494 + </data>
2495 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2496 + <value>Invalid format type specified. Supported format types are: json, table</value>
2497 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2498 + </data>
2499 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2500 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2504 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2505 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2508 + <value>Image '{}' not found, pulling</value>
2509 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2510 + </data>
2511 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2512 + <value>Environment variable key cannot be empty</value>
2513 + </data>
2514 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2515 + <value>Environment variable key '{}' cannot contain whitespace</value>
2516 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2517 + </data>
2518 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2519 + <value>Requested load but no input provided.</value>
2520 + </data>
2521 </root>
\ No newline at end of file
localization/strings/zh-CN/Resources.resw
+568
@@ -1956,4 +1956,572 @@ wsl.exe --manage &lt;DistributionName&gt; --set-sparse true --allow-unsafe</valu
1956 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1957 <value>Visual Studio 集成</value>
1958 </data>
1959 + <data name="MessageWslcUsage" xml:space="preserve">
1960 + <value>wslc - WSL 容器 CLI
1961 +用法:
1962 + wslc --help</value>
1963 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1964 + </data>
1965 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1966 + <value>找不到会话: '{}'</value>
1967 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1968 + </data>
1969 + <data name="MessageInvalidIp" xml:space="preserve">
1970 + <value>IP 地址 '{}' 无效</value>
1971 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1972 + </data>
1973 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1974 + <value>OpenSessionByName('{}') 失败</value>
1975 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1976 + </data>
1977 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1978 + <value>找不到 WSLC 会话。</value>
1979 + </data>
1980 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1981 + <value>找到 {} WSLC 会话{}:</value>
1982 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1983 + </data>
1984 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1985 + <value>会话终止失败: '{}'</value>
1986 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1987 + </data>
1988 + <data name="MessageWslcShellExited" xml:space="preserve">
1989 + <value>{} 退出,返回值: {}</value>
1990 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1991 + </data>
1992 + <data name="MessageWslcHeaderId" xml:space="preserve">
1993 + <value>ID</value>
1994 + </data>
1995 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1996 + <value>创建者 PID</value>
1997 + </data>
1998 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1999 + <value>显示名称</value>
2000 + </data>
2001 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2002 + <value>未知命令: '{}'</value>
2003 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2004 + </data>
2005 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2006 + <value>无法识别的命令:“{}”</value>
2007 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2008 + </data>
2009 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2010 + <value>未提供所需参数:“{}”</value>
2011 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2012 + </data>
2013 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2014 + <value>提供的参数超过允许的参数:“{}”</value>
2015 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2016 + </data>
2017 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2018 + <value>显示选定命令的帮助信息</value>
2019 + </data>
2020 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2021 + <value>已提供多个互相排斥的参数:“{}”</value>
2022 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2023 + </data>
2024 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2025 + <value>参数 {} 只能与 {} 一起使用</value>
2026 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2027 + </data>
2028 + <data name="WSLCCLI_Usage" xml:space="preserve">
2029 + <value>使用情况: {} {}</value>
2030 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2031 + </data>
2032 + <data name="WSLCCLI_Command" xml:space="preserve">
2033 + <value>命令</value>
2034 + </data>
2035 + <data name="WSLCCLI_Options" xml:space="preserve">
2036 + <value>选项</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2039 + <value>以下命令别名可用:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2042 + <value>下列命令有效:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2045 + <value>以下子命令可用:</value>
2046 + </data>
2047 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2048 + <value>下列选项可用:</value>
2049 + </data>
2050 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2051 + <value>以下参数可用:</value>
2052 + </data>
2053 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2054 + <value>如需特定命令的更多详细信息,请向其传递帮助参数。</value>
2055 + </data>
2056 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2057 + <value>无法识别当前命令的参数名称:“{}”</value>
2058 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2059 + </data>
2060 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2061 + <value>需要具有管理员权限才能执行此命令。</value>
2062 + </data>
2063 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2064 + <value>指定要使用的会话</value>
2065 + </data>
2066 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2067 + <value>附加到容器的 stdout/stderr</value>
2068 + </data>
2069 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2070 + <value>附加到 Stdin 并保持其打开状态</value>
2071 + </data>
2072 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2073 + <value>指定要使用的端口</value>
2074 + </data>
2075 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2076 + <value>容器 ID</value>
2077 + </data>
2078 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2079 + <value>缺少参数值:“{}”</value>
2080 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2081 + </data>
2082 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2083 + <value>无法识别当前命令的参数别名:“{}”</value>
2084 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2085 + </data>
2086 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2087 + <value>无效参数说明符:“{}”</value>
2088 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2089 + </data>
2090 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2091 + <value>未找到邻近标记别名:“{}”</value>
2092 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2093 + </data>
2094 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2095 + <value>邻近别名不是标志:“{}”</value>
2096 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2097 + </data>
2098 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2099 + <value>无效参数说明符:“{}”</value>
2100 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2101 + </data>
2102 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2103 + <value>标记参数不得包含邻近值:“{}”</value>
2104 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2105 + </data>
2106 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2107 + <value>在未预期的情况下找到位置参数:“{}”</value>
2108 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2109 + </data>
2110 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2111 + <value>“{}”处缺少参数名称</value>
2112 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2113 + </data>
2114 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2115 + <value>无法解析从参数“{}”开始的转发参数</value>
2116 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2117 + </data>
2118 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2119 + <value>遇到无效的额外参数:“{}”</value>
2120 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2121 + </data>
2122 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2123 + <value>具有值的别名参数必须是别名链中的最后一个: “{}”</value>
2124 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2125 + </data>
2126 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2127 + <value>版权所有 (c) Microsoft Corporation。保留所有权利。
2128 +有关此产品的隐私信息,请访问 https://aka.ms/privacy。</value>
2129 + <comment>Copyright notice and privacy link</comment>
2130 + </data>
2131 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2132 + <value>映像无效:“{}”</value>
2133 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2134 + </data>
2135 + <data name="MessageWslcInvalidName" xml:space="preserve">
2136 + <value>无效名称: '{}'</value>
2137 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2138 + </data>
2139 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2140 + <value>路径不是绝对路径:“{}”</value>
2141 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2142 + </data>
2143 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2144 + <value>找不到卷: '{}'</value>
2145 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2146 + </data>
2147 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2148 + <value>无效的卷选项: '{}'</value>
2149 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2150 + </data>
2151 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2152 + <value>不支持的卷类型: '{}'</value>
2153 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2154 + </data>
2155 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2156 + <value>卷 '{}' 正在使用中。</value>
2157 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2158 + </data>
2159 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2160 + <value>发现了 Dockerfile 和 Containerfile。使用 -f 选择要使用的文件</value>
2161 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2162 + </data>
2163 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2164 + <value>未能打开 {}': {}</value>
2165 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2166 + </data>
2167 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2168 + <value>在 '{}' 中找不到容器文件或 Dockerfile</value>
2169 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2170 + </data>
2171 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2172 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2173 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2174 + </data>
2175 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2176 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2177 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2178 + </data>
2179 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2180 + <value>Manage containers.</value>
2181 + </data>
2182 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2183 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2184 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2187 + <value>Attach to a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2190 + <value>Attaches to a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2193 + <value>Create a container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2196 + <value>Creates a container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2199 + <value>Execute a command in a running container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2202 + <value>Executes a command in a running container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2205 + <value>Inspect a container.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2208 + <value>Display detailed information about a container.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2211 + <value>Kill containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2214 + <value>Kills containers.</value>
2215 + </data>
2216 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2217 + <value>List containers.</value>
2218 + </data>
2219 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2220 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2221 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2224 + <value>View container logs.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2227 + <value>View logs for a container.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2230 + <value>Remove containers.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2233 + <value>Removes containers.</value>
2234 + </data>
2235 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2236 + <value>Run a container.</value>
2237 + </data>
2238 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2239 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2240 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2243 + <value>Start a container.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2246 + <value>Starts a container.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2249 + <value>Stop containers.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2252 + <value>Stops containers.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2255 + <value>Manage images.</value>
2256 + </data>
2257 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2258 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2259 + </data>
2260 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2261 + <value>Build an image from a Dockerfile.</value>
2262 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2263 + </data>
2264 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2265 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2266 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2267 + </data>
2268 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2269 + <value>Inspect images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2272 + <value>Inspect images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2275 + <value>List images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2278 + <value>Lists images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2281 + <value>Load images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2284 + <value>Loads images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2287 + <value>Pull images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2290 + <value>Pulls images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2293 + <value>Remove images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2296 + <value>Removes images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2299 + <value>Save images.</value>
2300 + </data>
2301 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2302 + <value>Saves images.</value>
2303 + </data>
2304 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2305 + <value>Manage sessions.</value>
2306 + </data>
2307 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2308 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2309 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2310 + </data>
2311 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2312 + <value>List sessions.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2315 + <value>Lists active session(s).</value>
2316 + </data>
2317 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2318 + <value>Attach to a session.</value>
2319 + </data>
2320 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2321 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2322 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2323 + </data>
2324 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2325 + <value>Terminate a session.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2328 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2329 + </data>
2330 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2331 + <value>Open the settings file in the default editor.</value>
2332 + </data>
2333 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2334 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2335 +On first run, creates the file with all settings commented out at their defaults.</value>
2336 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2339 + <value>Reset settings to built-in defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2342 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2343 + </data>
2344 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2345 + <value>Settings reset to defaults.</value>
2346 + </data>
2347 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2348 + <value>Show all regardless of state.</value>
2349 + </data>
2350 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2351 + <value>Set build-time variables (KEY=VALUE)</value>
2352 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2353 + </data>
2354 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2355 + <value>The command to run</value>
2356 + </data>
2357 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2358 + <value>Delete containers even if they are running</value>
2359 + </data>
2360 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2361 + <value>Run container in detached mode</value>
2362 + </data>
2363 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2364 + <value>Specifies the container init process executable</value>
2365 + </data>
2366 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2367 + <value>Key=Value pairs for environment variables</value>
2368 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2369 + </data>
2370 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2371 + <value>File containing key=value pairs of env variables</value>
2372 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2373 + </data>
2374 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2375 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2376 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2377 + </data>
2378 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2379 + <value>Follow log output</value>
2380 + </data>
2381 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2382 + <value>Output formatting (json or table) (Default: table)</value>
2383 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2384 + </data>
2385 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2386 + <value>Arguments to pass to container's init process</value>
2387 + </data>
2388 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2389 + <value>Delete images even if they are being used</value>
2390 + </data>
2391 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2392 + <value>Image name</value>
2393 + </data>
2394 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2395 + <value>Provides path to the tar archive file containing the image</value>
2396 + </data>
2397 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2398 + <value>Name of the container</value>
2399 + </data>
2400 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2401 + <value>Do not delete untagged parents</value>
2402 + </data>
2403 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2404 + <value>Do not truncate output</value>
2405 + </data>
2406 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2407 + <value>Path for the saved image</value>
2408 + </data>
2409 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2410 + <value>Path to the build context directory</value>
2411 + </data>
2412 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2413 + <value>Publish a port from a container to host</value>
2414 + </data>
2415 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2416 + <value>Outputs the container IDs only</value>
2417 + </data>
2418 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2419 + <value>Remove the container after it stops</value>
2420 + </data>
2421 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2422 + <value>Session ID</value>
2423 + </data>
2424 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2425 + <value>Signal to send (default: {})</value>
2426 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2427 + </data>
2428 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2429 + <value>Tag for the built image</value>
2430 + </data>
2431 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2432 + <value>Time in seconds to wait before executing (default 5)</value>
2433 + </data>
2434 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2435 + <value>Open a TTY with the container process.</value>
2436 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2437 + </data>
2438 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2439 + <value>Output verbose details</value>
2440 + </data>
2441 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2442 + <value>Show version information for this tool</value>
2443 + </data>
2444 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2445 + <value>Bind mount a volume to the container</value>
2446 + </data>
2447 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2448 + <value>Write the container ID to the provided path.</value>
2449 + </data>
2450 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2451 + <value>IP address of the DNS nameserver in resolv.conf</value>
2452 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2453 + </data>
2454 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2455 + <value>Set the default DNS Domain</value>
2456 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2457 + </data>
2458 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2459 + <value>Set DNS options</value>
2460 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2461 + </data>
2462 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2463 + <value>Set DNS search domains</value>
2464 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2465 + </data>
2466 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2467 + <value>Group Id for the process</value>
2468 + </data>
2469 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2470 + <value>No configuration of DNS in the container</value>
2471 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2472 + </data>
2473 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2474 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2475 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2476 + </data>
2477 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2478 + <value>Image pull policy (always|missing|never) (default:never)</value>
2479 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2480 + </data>
2481 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2482 + <value>Use this scheme for registry connection</value>
2483 + </data>
2484 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2485 + <value>Mount tmpfs to the container at the given path</value>
2486 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2487 + </data>
2488 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2489 + <value>User ID for the process (name|uid|uid:gid)</value>
2490 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2491 + </data>
2492 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2493 + <value>Expose virtualization capabilities to the container</value>
2494 + </data>
2495 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2496 + <value>Arguments to pass to the command being executed inside the container</value>
2497 + </data>
2498 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2499 + <value>Show detailed information about the listed sessions.</value>
2500 + </data>
2501 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2502 + <value>Invalid format type specified. Supported format types are: json, table</value>
2503 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2504 + </data>
2505 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2506 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2507 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2508 + </data>
2509 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2510 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2511 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2512 + </data>
2513 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2514 + <value>Image '{}' not found, pulling</value>
2515 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2516 + </data>
2517 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2518 + <value>Environment variable key cannot be empty</value>
2519 + </data>
2520 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2521 + <value>Environment variable key '{}' cannot contain whitespace</value>
2522 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2523 + </data>
2524 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2525 + <value>Requested load but no input provided.</value>
2526 + </data>
2527 </root>
\ No newline at end of file
localization/strings/zh-TW/Resources.resw
+568
@@ -1956,4 +1956,572 @@ wsl.exe --manage &lt;DistributionName&gt; --set-sparse true --allow-unsafe</valu
1956 <data name="Settings_Shell_VSIntegration.Content" xml:space="preserve">
1957 <value>Visual Studio 整合</value>
1958 </data>
1959 + <data name="MessageWslcUsage" xml:space="preserve">
1960 + <value>wslc - WSL 容器 CLI
1961 +使用方式:
1962 + wslc --help</value>
1963 + <comment>{Locked="--help"}Command line arguments, file names and string inserts should not be translated</comment>
1964 + </data>
1965 + <data name="MessageWslcSessionNotFound" xml:space="preserve">
1966 + <value>找不到工作階段: '{}'</value>
1967 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1968 + </data>
1969 + <data name="MessageInvalidIp" xml:space="preserve">
1970 + <value>無效的 IP 位址 '{}'</value>
1971 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1972 + </data>
1973 + <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
1974 + <value>OpenSessionByName('{}') 失敗</value>
1975 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1976 + </data>
1977 + <data name="MessageWslcNoSessionsFound" xml:space="preserve">
1978 + <value>未找到 WSLC 工作階段。</value>
1979 + </data>
1980 + <data name="MessageWslcSessionsFound" xml:space="preserve">
1981 + <value>找到 {} WSLC 工作階段{}:</value>
1982 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1983 + </data>
1984 + <data name="MessageWslcTerminateSessionFailed" xml:space="preserve">
1985 + <value>工作階段終止失敗: '{}'</value>
1986 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1987 + </data>
1988 + <data name="MessageWslcShellExited" xml:space="preserve">
1989 + <value>{} 結束: {}</value>
1990 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1991 + </data>
1992 + <data name="MessageWslcHeaderId" xml:space="preserve">
1993 + <value>識別碼</value>
1994 + </data>
1995 + <data name="MessageWslcHeaderCreatorPid" xml:space="preserve">
1996 + <value>建立者 PID</value>
1997 + </data>
1998 + <data name="MessageWslcHeaderDisplayName" xml:space="preserve">
1999 + <value>顯示名稱</value>
2000 + </data>
2001 + <data name="MessageWslcUnknownCommand" xml:space="preserve">
2002 + <value>未知的命令: '{}'</value>
2003 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2004 + </data>
2005 + <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2006 + <value>無法辨識的命令: '{}'</value>
2007 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2008 + </data>
2009 + <data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
2010 + <value>未提供必要的引數: '{}'</value>
2011 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2012 + </data>
2013 + <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2014 + <value>引數提供的次數超過允許的次數: '{}'</value>
2015 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2016 + </data>
2017 + <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2018 + <value>顯示所選命令的相關說明</value>
2019 + </data>
2020 + <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2021 + <value>已提供多個互斥的引數: '{}'</value>
2022 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2023 + </data>
2024 + <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
2025 + <value>參數 {} 僅能與 {} 一起使用</value>
2026 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2027 + </data>
2028 + <data name="WSLCCLI_Usage" xml:space="preserve">
2029 + <value>使用量: {}{}</value>
2030 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2031 + </data>
2032 + <data name="WSLCCLI_Command" xml:space="preserve">
2033 + <value>命令</value>
2034 + </data>
2035 + <data name="WSLCCLI_Options" xml:space="preserve">
2036 + <value>選項</value>
2037 + </data>
2038 + <data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
2039 + <value>以下命令別名可用:</value>
2040 + </data>
2041 + <data name="WSLCCLI_AvailableCommands" xml:space="preserve">
2042 + <value>以下是所有可用的命令:</value>
2043 + </data>
2044 + <data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
2045 + <value>以下是可用的子命令:</value>
2046 + </data>
2047 + <data name="WSLCCLI_AvailableOptions" xml:space="preserve">
2048 + <value>以下是可用的選項:</value>
2049 + </data>
2050 + <data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2051 + <value>以下是可用的引數:</value>
2052 + </data>
2053 + <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2054 + <value>如需特定命令的更多詳細資料,請向其傳遞說明引數。</value>
2055 + </data>
2056 + <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2057 + <value>無法辨識目前命令的引數名稱: '{}'</value>
2058 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2059 + </data>
2060 + <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
2061 + <value>此命令需要系統管理員許可權才能執行。</value>
2062 + </data>
2063 + <data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
2064 + <value>指定要使用的工作階段</value>
2065 + </data>
2066 + <data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
2067 + <value>附加到容器的 stdout/stderr</value>
2068 + </data>
2069 + <data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
2070 + <value>附加到標準輸入並保持開啟</value>
2071 + </data>
2072 + <data name="WSLCCLI_PortArgDescription" xml:space="preserve">
2073 + <value>指定要使用的連接埠</value>
2074 + </data>
2075 + <data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
2076 + <value>容器識別碼</value>
2077 + </data>
2078 + <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2079 + <value>遺失引數值: '{}'</value>
2080 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2081 + </data>
2082 + <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2083 + <value>無法辨識目前命令的引數別名: '{}'</value>
2084 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2085 + </data>
2086 + <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2087 + <value>無效的引數指定元: '{}'</value>
2088 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2089 + </data>
2090 + <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2091 + <value>找不到鄰近旗標別名: '{}'</value>
2092 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2093 + </data>
2094 + <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2095 + <value>鄰近別名不是旗標: '{}'</value>
2096 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2097 + </data>
2098 + <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2099 + <value>無效的引數指定元: '{}'</value>
2100 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2101 + </data>
2102 + <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2103 + <value>旗標引數不能包含鄰近值: '{}'</value>
2104 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2105 + </data>
2106 + <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2107 + <value>在未預期的情況下找到位置引數: '{}'</value>
2108 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2109 + </data>
2110 + <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2111 + <value>遺失參數名稱於: '{}'</value>
2112 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2113 + </data>
2114 + <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
2115 + <value>無法解析從參數 '{}' 開始的轉發參數</value>
2116 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2117 + </data>
2118 + <data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
2119 + <value>遇到無效的額外參數: '{}'</value>
2120 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2121 + </data>
2122 + <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2123 + <value>具有值的別名參數必須位於別名鏈的最後: '{}'</value>
2124 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2125 + </data>
2126 + <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
2127 + <value>著作權 (c) Microsoft Corporation。著作權所有,並保留一切權利。
2128 +如需此產品的隱私權資訊,請瀏覽 https://aka.ms/privacy。</value>
2129 + <comment>Copyright notice and privacy link</comment>
2130 + </data>
2131 + <data name="MessageWslcInvalidImage" xml:space="preserve">
2132 + <value>不正確的影像: '{}'</value>
2133 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2134 + </data>
2135 + <data name="MessageWslcInvalidName" xml:space="preserve">
2136 + <value>無效名稱: '{}'</value>
2137 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2138 + </data>
2139 + <data name="MessagePathNotAbsolute" xml:space="preserve">
2140 + <value>路徑不是絕對路徑: '{}'</value>
2141 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2142 + </data>
2143 + <data name="MessageWslcVolumeNotFound" xml:space="preserve">
2144 + <value>找不到磁碟區: '{}'</value>
2145 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2146 + </data>
2147 + <data name="MessageWslcInvalidVolumeOptions" xml:space="preserve">
2148 + <value>無效的音量選項: '{}'</value>
2149 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2150 + </data>
2151 + <data name="MessageWslcInvalidVolumeType" xml:space="preserve">
2152 + <value>不支援的音量類型: '{}'</value>
2153 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2154 + </data>
2155 + <data name="MessageWslcVolumeInUse" xml:space="preserve">
2156 + <value>磁碟區 '{}' 使用中。</value>
2157 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2158 + </data>
2159 + <data name="MessageWslcBothDockerAndContainerFileFound" xml:space="preserve">
2160 + <value>同時找到 Dockerfile 和 Containerfile。使用 -f 以選取要使用的檔案</value>
2161 + <comment>{FixedPlaceholder="Dockerfile"}{FixedPlaceholder="Containerfile"}{FixedPlaceholder="-f"}Command line arguments, file names and string inserts should not be translated</comment>
2162 + </data>
2163 + <data name="MessageWslcFailedToOpenFile" xml:space="preserve">
2164 + <value>無法開啟 '{}': {}</value>
2165 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2166 + </data>
2167 + <data name="MessageWslcBuildFileNotFound" xml:space="preserve">
2168 + <value>在 '{}' 中找不到 Containerfile 或 Dockerfile</value>
2169 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2170 + </data>
2171 + <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2172 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2173 + <comment>{Locked="WSLC"}Product names should not be translated</comment>
2174 + </data>
2175 + <data name="WSLCCLI_RootCommandLongDesc" xml:space="preserve">
2176 + <value>WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers from the command line.</value>
2177 + <comment>{Locked="WSLC"}{Locked="WSL"}Product names should not be translated</comment>
2178 + </data>
2179 + <data name="WSLCCLI_ContainerCommandDesc" xml:space="preserve">
2180 + <value>Manage containers.</value>
2181 + </data>
2182 + <data name="WSLCCLI_ContainerCommandLongDesc" xml:space="preserve">
2183 + <value>Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them.</value>
2184 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2185 + </data>
2186 + <data name="WSLCCLI_ContainerAttachDesc" xml:space="preserve">
2187 + <value>Attach to a container.</value>
2188 + </data>
2189 + <data name="WSLCCLI_ContainerAttachLongDesc" xml:space="preserve">
2190 + <value>Attaches to a container.</value>
2191 + </data>
2192 + <data name="WSLCCLI_ContainerCreateDesc" xml:space="preserve">
2193 + <value>Create a container.</value>
2194 + </data>
2195 + <data name="WSLCCLI_ContainerCreateLongDesc" xml:space="preserve">
2196 + <value>Creates a container.</value>
2197 + </data>
2198 + <data name="WSLCCLI_ContainerExecDesc" xml:space="preserve">
2199 + <value>Execute a command in a running container.</value>
2200 + </data>
2201 + <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2202 + <value>Executes a command in a running container.</value>
2203 + </data>
2204 + <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2205 + <value>Inspect a container.</value>
2206 + </data>
2207 + <data name="WSLCCLI_ContainerInspectLongDesc" xml:space="preserve">
2208 + <value>Display detailed information about a container.</value>
2209 + </data>
2210 + <data name="WSLCCLI_ContainerKillDesc" xml:space="preserve">
2211 + <value>Kill containers.</value>
2212 + </data>
2213 + <data name="WSLCCLI_ContainerKillLongDesc" xml:space="preserve">
2214 + <value>Kills containers.</value>
2215 + </data>
2216 + <data name="WSLCCLI_ContainerListDesc" xml:space="preserve">
2217 + <value>List containers.</value>
2218 + </data>
2219 + <data name="WSLCCLI_ContainerListLongDesc" xml:space="preserve">
2220 + <value>Lists containers. By default, only running containers are shown; use --all to include all containers.</value>
2221 + <comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
2222 + </data>
2223 + <data name="WSLCCLI_ContainerLogsDesc" xml:space="preserve">
2224 + <value>View container logs.</value>
2225 + </data>
2226 + <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2227 + <value>View logs for a container.</value>
2228 + </data>
2229 + <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2230 + <value>Remove containers.</value>
2231 + </data>
2232 + <data name="WSLCCLI_ContainerRemoveLongDesc" xml:space="preserve">
2233 + <value>Removes containers.</value>
2234 + </data>
2235 + <data name="WSLCCLI_ContainerRunDesc" xml:space="preserve">
2236 + <value>Run a container.</value>
2237 + </data>
2238 + <data name="WSLCCLI_ContainerRunLongDesc" xml:space="preserve">
2239 + <value>Runs a container. By default, the container is started in the background; use --detach to run in the foreground.</value>
2240 + <comment>{Locked="--detach "}Command line arguments, file names and string inserts should not be translated</comment>
2241 + </data>
2242 + <data name="WSLCCLI_ContainerStartDesc" xml:space="preserve">
2243 + <value>Start a container.</value>
2244 + </data>
2245 + <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2246 + <value>Starts a container.</value>
2247 + </data>
2248 + <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2249 + <value>Stop containers.</value>
2250 + </data>
2251 + <data name="WSLCCLI_ContainerStopLongDesc" xml:space="preserve">
2252 + <value>Stops containers.</value>
2253 + </data>
2254 + <data name="WSLCCLI_ImageCommandDesc" xml:space="preserve">
2255 + <value>Manage images.</value>
2256 + </data>
2257 + <data name="WSLCCLI_ImageCommandLongDesc" xml:space="preserve">
2258 + <value>Manage container images, including building, pulling, listing, and removing them.</value>
2259 + </data>
2260 + <data name="WSLCCLI_ImageBuildDesc" xml:space="preserve">
2261 + <value>Build an image from a Dockerfile.</value>
2262 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2263 + </data>
2264 + <data name="WSLCCLI_ImageBuildLongDesc" xml:space="preserve">
2265 + <value>Builds an image from a Dockerfile and a build context directory.</value>
2266 + <comment>{Locked="Dockerfile"}Command line arguments should not be translated</comment>
2267 + </data>
2268 + <data name="WSLCCLI_ImageInspectDesc" xml:space="preserve">
2269 + <value>Inspect images.</value>
2270 + </data>
2271 + <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2272 + <value>Inspect images.</value>
2273 + </data>
2274 + <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2275 + <value>List images.</value>
2276 + </data>
2277 + <data name="WSLCCLI_ImageListLongDesc" xml:space="preserve">
2278 + <value>Lists images.</value>
2279 + </data>
2280 + <data name="WSLCCLI_ImageLoadDesc" xml:space="preserve">
2281 + <value>Load images.</value>
2282 + </data>
2283 + <data name="WSLCCLI_ImageLoadLongDesc" xml:space="preserve">
2284 + <value>Loads images.</value>
2285 + </data>
2286 + <data name="WSLCCLI_ImagePullDesc" xml:space="preserve">
2287 + <value>Pull images.</value>
2288 + </data>
2289 + <data name="WSLCCLI_ImagePullLongDesc" xml:space="preserve">
2290 + <value>Pulls images.</value>
2291 + </data>
2292 + <data name="WSLCCLI_ImageRemoveDesc" xml:space="preserve">
2293 + <value>Remove images.</value>
2294 + </data>
2295 + <data name="WSLCCLI_ImageRemoveLongDesc" xml:space="preserve">
2296 + <value>Removes images.</value>
2297 + </data>
2298 + <data name="WSLCCLI_ImageSaveDesc" xml:space="preserve">
2299 + <value>Save images.</value>
2300 + </data>
2301 + <data name="WSLCCLI_ImageSaveLongDesc" xml:space="preserve">
2302 + <value>Saves images.</value>
2303 + </data>
2304 + <data name="WSLCCLI_SessionCommandDesc" xml:space="preserve">
2305 + <value>Manage sessions.</value>
2306 + </data>
2307 + <data name="WSLCCLI_SessionCommandLongDesc" xml:space="preserve">
2308 + <value>Manage WSL container sessions, including listing active sessions and launching interactive shells.</value>
2309 + <comment>{Locked="WSL"}Product names should not be translated</comment>
2310 + </data>
2311 + <data name="WSLCCLI_SessionListDesc" xml:space="preserve">
2312 + <value>List sessions.</value>
2313 + </data>
2314 + <data name="WSLCCLI_SessionListLongDesc" xml:space="preserve">
2315 + <value>Lists active session(s).</value>
2316 + </data>
2317 + <data name="WSLCCLI_SessionShellDesc" xml:space="preserve">
2318 + <value>Attach to a session.</value>
2319 + </data>
2320 + <data name="WSLCCLI_SessionShellLongDesc" xml:space="preserve">
2321 + <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2322 + <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2323 + </data>
2324 + <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2325 + <value>Terminate a session.</value>
2326 + </data>
2327 + <data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
2328 + <value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
2329 + </data>
2330 + <data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
2331 + <value>Open the settings file in the default editor.</value>
2332 + </data>
2333 + <data name="WSLCCLI_SettingsCommandLongDesc" xml:space="preserve">
2334 + <value>Opens the wslc user settings file in the system default editor for .yaml files.
2335 +On first run, creates the file with all settings commented out at their defaults.</value>
2336 + <comment>{Locked="wslc"}{Locked=".yaml"}Command line arguments should not be translated</comment>
2337 + </data>
2338 + <data name="WSLCCLI_SettingsResetDesc" xml:space="preserve">
2339 + <value>Reset settings to built-in defaults.</value>
2340 + </data>
2341 + <data name="WSLCCLI_SettingsResetLongDesc" xml:space="preserve">
2342 + <value>Overwrites the settings file with a commented-out defaults template.</value>
2343 + </data>
2344 + <data name="WSLCCLI_SettingsResetConfirm" xml:space="preserve">
2345 + <value>Settings reset to defaults.</value>
2346 + </data>
2347 + <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2348 + <value>Show all regardless of state.</value>
2349 + </data>
2350 + <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2351 + <value>Set build-time variables (KEY=VALUE)</value>
2352 + <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
2353 + </data>
2354 + <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2355 + <value>The command to run</value>
2356 + </data>
2357 + <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2358 + <value>Delete containers even if they are running</value>
2359 + </data>
2360 + <data name="WSLCCLI_DetachArgDescription" xml:space="preserve">
2361 + <value>Run container in detached mode</value>
2362 + </data>
2363 + <data name="WSLCCLI_EntrypointArgDescription" xml:space="preserve">
2364 + <value>Specifies the container init process executable</value>
2365 + </data>
2366 + <data name="WSLCCLI_EnvArgDescription" xml:space="preserve">
2367 + <value>Key=Value pairs for environment variables</value>
2368 + <comment>{Locked="Key=Value"}Command line arguments should not be translated</comment>
2369 + </data>
2370 + <data name="WSLCCLI_EnvFileArgDescription" xml:space="preserve">
2371 + <value>File containing key=value pairs of env variables</value>
2372 + <comment>{Locked="key=value"}Command line arguments should not be translated</comment>
2373 + </data>
2374 + <data name="WSLCCLI_FileArgDescription" xml:space="preserve">
2375 + <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2376 + <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2377 + </data>
2378 + <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2379 + <value>Follow log output</value>
2380 + </data>
2381 + <data name="WSLCCLI_FormatArgDescription" xml:space="preserve">
2382 + <value>Output formatting (json or table) (Default: table)</value>
2383 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2384 + </data>
2385 + <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2386 + <value>Arguments to pass to container's init process</value>
2387 + </data>
2388 + <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2389 + <value>Delete images even if they are being used</value>
2390 + </data>
2391 + <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2392 + <value>Image name</value>
2393 + </data>
2394 + <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2395 + <value>Provides path to the tar archive file containing the image</value>
2396 + </data>
2397 + <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2398 + <value>Name of the container</value>
2399 + </data>
2400 + <data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2401 + <value>Do not delete untagged parents</value>
2402 + </data>
2403 + <data name="WSLCCLI_NoTruncArgDescription" xml:space="preserve">
2404 + <value>Do not truncate output</value>
2405 + </data>
2406 + <data name="WSLCCLI_OutputArgDescription" xml:space="preserve">
2407 + <value>Path for the saved image</value>
2408 + </data>
2409 + <data name="WSLCCLI_PathArgDescription" xml:space="preserve">
2410 + <value>Path to the build context directory</value>
2411 + </data>
2412 + <data name="WSLCCLI_PublishArgDescription" xml:space="preserve">
2413 + <value>Publish a port from a container to host</value>
2414 + </data>
2415 + <data name="WSLCCLI_QuietArgDescription" xml:space="preserve">
2416 + <value>Outputs the container IDs only</value>
2417 + </data>
2418 + <data name="WSLCCLI_RemoveArgDescription" xml:space="preserve">
2419 + <value>Remove the container after it stops</value>
2420 + </data>
2421 + <data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
2422 + <value>Session ID</value>
2423 + </data>
2424 + <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
2425 + <value>Signal to send (default: {})</value>
2426 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2427 + </data>
2428 + <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2429 + <value>Tag for the built image</value>
2430 + </data>
2431 + <data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
2432 + <value>Time in seconds to wait before executing (default 5)</value>
2433 + </data>
2434 + <data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
2435 + <value>Open a TTY with the container process.</value>
2436 + <comment>{Locked="TTY"}Command line arguments should not be translated</comment>
2437 + </data>
2438 + <data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
2439 + <value>Output verbose details</value>
2440 + </data>
2441 + <data name="WSLCCLI_VersionArgDescription" xml:space="preserve">
2442 + <value>Show version information for this tool</value>
2443 + </data>
2444 + <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2445 + <value>Bind mount a volume to the container</value>
2446 + </data>
2447 + <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2448 + <value>Write the container ID to the provided path.</value>
2449 + </data>
2450 + <data name="WSLCCLI_DNSArgDescription" xml:space="preserve">
2451 + <value>IP address of the DNS nameserver in resolv.conf</value>
2452 + <comment>{Locked="resolv.conf"}Command line arguments should not be translated</comment>
2453 + </data>
2454 + <data name="WSLCCLI_DNSDomainArgDescription" xml:space="preserve">
2455 + <value>Set the default DNS Domain</value>
2456 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2457 + </data>
2458 + <data name="WSLCCLI_DNSOptionArgDescription" xml:space="preserve">
2459 + <value>Set DNS options</value>
2460 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2461 + </data>
2462 + <data name="WSLCCLI_DNSSearchArgDescription" xml:space="preserve">
2463 + <value>Set DNS search domains</value>
2464 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2465 + </data>
2466 + <data name="WSLCCLI_GroupIdArgDescription" xml:space="preserve">
2467 + <value>Group Id for the process</value>
2468 + </data>
2469 + <data name="WSLCCLI_NoDNSArgDescription" xml:space="preserve">
2470 + <value>No configuration of DNS in the container</value>
2471 + <comment>{Locked="DNS"}Command line arguments should not be translated</comment>
2472 + </data>
2473 + <data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
2474 + <value>Progress type (format: none|ansi) (default: ansi)</value>
2475 + <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
2476 + </data>
2477 + <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
2478 + <value>Image pull policy (always|missing|never) (default:never)</value>
2479 + <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
2480 + </data>
2481 + <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
2482 + <value>Use this scheme for registry connection</value>
2483 + </data>
2484 + <data name="WSLCCLI_TMPFSArgDescription" xml:space="preserve">
2485 + <value>Mount tmpfs to the container at the given path</value>
2486 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2487 + </data>
2488 + <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2489 + <value>User ID for the process (name|uid|uid:gid)</value>
2490 + <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
2491 + </data>
2492 + <data name="WSLCCLI_VirtualArgDescription" xml:space="preserve">
2493 + <value>Expose virtualization capabilities to the container</value>
2494 + </data>
2495 + <data name="WSLCCLI_ContainerExecForwardArgsDescription" xml:space="preserve">
2496 + <value>Arguments to pass to the command being executed inside the container</value>
2497 + </data>
2498 + <data name="WSLCCLI_SessionListVerboseArgDescription" xml:space="preserve">
2499 + <value>Show detailed information about the listed sessions.</value>
2500 + </data>
2501 + <data name="WSLCCLI_InvalidFormatError" xml:space="preserve">
2502 + <value>Invalid format type specified. Supported format types are: json, table</value>
2503 + <comment>{Locked="json"}{Locked="table"}Command line arguments should not be translated</comment>
2504 + </data>
2505 + <data name="WSLCCLI_InvalidSignalError" xml:space="preserve">
2506 + <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2507 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2508 + </data>
2509 + <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2510 + <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2511 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2512 + </data>
2513 + <data name="WSLCCLI_ImageNotFoundPulling" xml:space="preserve">
2514 + <value>Image '{}' not found, pulling</value>
2515 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2516 + </data>
2517 + <data name="WSLCCLI_EnvKeyEmptyError" xml:space="preserve">
2518 + <value>Environment variable key cannot be empty</value>
2519 + </data>
2520 + <data name="WSLCCLI_EnvKeyWhitespaceError" xml:space="preserve">
2521 + <value>Environment variable key '{}' cannot contain whitespace</value>
2522 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2523 + </data>
2524 + <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
2525 + <value>Requested load but no input provided.</value>
2526 + </data>
2527 </root>
\ No newline at end of file
msipackage/CMakeLists.txt
+7 -2
@@ -8,11 +8,16 @@ else()
8 set(PACKAGE_INPUT_DIR ${BIN})
9 endif()
10
11 +# DCOM Access/Launch permission binary security descriptor.
12 +# SDDL: O:BAG:BAD:(A;;CCDCSW;;;AU)(A;;CCDCSW;;;PS)(A;;CCDCSW;;;SY)
13 +# Grants local launch/activate to Authenticated Users, Principal Self, and SYSTEM.
14 +set(DCOM_PERMISSION "01000480580000006800000000000000140000000200440003000000000014000B00000001010000000000050B000000000014000B00000001010000000000050A000000000014000B0000000101000000000005120000000102000000000005200000002002000001020000000000052000000020020000")
15 +
16 set(OUTPUT_PACKAGE ${BIN}/wsl.msi)
17 set(PACKAGE_WIX_IN ${CMAKE_CURRENT_LIST_DIR}/package.wix.in)
18 set(PACKAGE_WIX ${BIN}/package.wix)
19 set(CAB_CACHE ${BIN}/cab)
15 -set(WINDOWS_BINARIES wsl.exe;wslg.exe;wslhost.exe;wslrelay.exe;wslservice.exe;wslserviceproxystub.dll;wslinstall.dll)
20 +set(WINDOWS_BINARIES wsl.exe;wslg.exe;wslhost.exe;wslrelay.exe;wslservice.exe;wslserviceproxystub.dll;wslinstall.dll;wslc.exe;wslcsession.exe)
21 if (WSL_BUILD_WSL_SETTINGS)
22 list(APPEND WINDOWS_BINARIES "wslsettings/wslsettings.dll;wslsettings/wslsettings.exe;libwsl.dll")
23 endif()
@@ -52,7 +57,7 @@ add_custom_command(
57
58 add_custom_target(msipackage DEPENDS ${OUTPUT_PACKAGE})
59 set_target_properties(msipackage PROPERTIES EXCLUDE_FROM_ALL FALSE SOURCES ${PACKAGE_WIX_IN})
55 -add_dependencies(msipackage wsl wslg wslservice wslhost wslrelay wslserviceproxystub init initramfs wslinstall msixgluepackage)
60 +add_dependencies(msipackage wsl wslg wslservice wslhost wslrelay wslserviceproxystub init initramfs wslinstall msixgluepackage wslc wslcsession)
61
62 if (WSL_BUILD_WSL_SETTINGS)
63 add_dependencies(msipackage wslsettings libwsl)
msipackage/package.wix.in
+194 -64
@@ -27,6 +27,7 @@
27 </File>
28
29 <File Id="wslg.exe" Name="wslg.exe" Source="${PACKAGE_INPUT_DIR}/wslg.exe" />
30 + <File Id="wslc.exe" Name="wslc.exe" Source="${PACKAGE_INPUT_DIR}/wslc.exe" />
31 <File Id="wslhost.exe" Name="wslhost.exe" Source="${PACKAGE_INPUT_DIR}/wslhost.exe" />
32 <File Id="wslrelay.exe" Name="wslrelay.exe" Source="${PACKAGE_INPUT_DIR}/wslrelay.exe" />
33 <File Id="wslserviceproxystub.dll" Name="wslserviceproxystub.dll" Source="${PACKAGE_INPUT_DIR}/wslserviceproxystub.dll" />
@@ -40,85 +41,100 @@
41 <File Id="system.vhd" Source="${WSLG_SOURCE_DIR}/${TARGET_PLATFORM}/system.vhd"/>
42 <?endif?>
43
44 + <!-- Add INSTALLDIR to system PATH to make wslc.exe and other CLI tools accessible. -->
45 + <!-- NOTE:
46 + - Permanent="no" ensures this PATH entry is removed when the MSI is uninstalled.
47 + - If INSTALLDIR is manually moved or deleted after installation (outside of MSI),
48 + the PATH entry will become stale until uninstall or manual correction.
49 + - This behavior is intentional and currently implemented as a convenience until
50 + we can deploy these CLI tools (e.g. wslc.exe) to a stable system location
51 + such as System32, or offer an installer option to skip PATH modification. -->
52 + <Environment Id="PATH" Name="PATH" Value="[INSTALLDIR]" Permanent="no" Part="last" Action="set" System="yes" />
53 +
54 <!-- Installation folder -->
55 <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\MSI">
56 <RegistryValue Name="InstallLocation" Value="[INSTALLDIR]" Type="string" />
57 <RegistryValue Name="ProductCode" Value="[ProductCode]" Type="string" />
58 <RegistryValue Name="Version" Value="${PACKAGE_VERSION}" Type="string" />
59 </RegistryKey>
60 +
61 + <!-- DCAT registration -->
62 + <RegistryKey Root="HKLM" Key="${DCAT_REGISTRATION_KEY}">
63 + <RegistryValue Name="Version" Value="${PACKAGE_VERSION}" Type="string" />
64 + </RegistryKey>
65 </Component>
66
67 <Component Id="explorerplan9shortcut" Guid="{93CBFF23-A04C-4344-A332-238CE5B97AED}" UninstallWhenSuperseded="yes" DisableRegistryReflection="yes" Bitness="always64">
68 <!-- Explorer extensions -->
53 - <RegistryKey Root="HKLM" Key="SOFTWARE\Classes\CLSID\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}">
54 - <RegistryValue Value="Linux" Type="string"/>
55 - <RegistryValue Name="SortOrderIndex" Value="119" Type="integer"/>
56 - <!--0x77-->
57 - <RegistryValue Name="System.IsPinnedToNameSpaceTree" Value="1" Type="integer"/>
58 -
59 - <RegistryKey Key="DefaultIcon">
60 - <RegistryValue Value="[System64Folder]wsl.exe,-1" Type="string"/>
61 - </RegistryKey>
69 + <RegistryKey Root="HKLM" Key="SOFTWARE\Classes\CLSID\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}">
70 + <RegistryValue Value="Linux" Type="string"/>
71 + <RegistryValue Name="SortOrderIndex" Value="119" Type="integer"/>
72 + <!--0x77-->
73 + <RegistryValue Name="System.IsPinnedToNameSpaceTree" Value="1" Type="integer"/>
74 +
75 + <RegistryKey Key="DefaultIcon">
76 + <RegistryValue Value="[System64Folder]wsl.exe,-1" Type="string"/>
77 + </RegistryKey>
78
63 - <RegistryKey Key="InProcServer32">
64 - <RegistryValue Value="[System64Folder]windows.storage.dll" Type="string"/>
65 - </RegistryKey>
79 + <RegistryKey Key="InProcServer32">
80 + <RegistryValue Value="[System64Folder]windows.storage.dll" Type="string"/>
81 + </RegistryKey>
82
67 - <RegistryKey Key="ShellFolder">
68 - <RegistryValue Name="Attributes" Value="2692743245" Type="integer"/>
69 - <!--0xa080004d"-->
70 - <RegistryValue Name="FolderValueFlags" Value="40" Type="integer"/>
71 - <!--0x28-->
72 - </RegistryKey>
83 + <RegistryKey Key="ShellFolder">
84 + <RegistryValue Name="Attributes" Value="2692743245" Type="integer"/>
85 + <!--0xa080004d"-->
86 + <RegistryValue Name="FolderValueFlags" Value="40" Type="integer"/>
87 + <!--0x28-->
88 + </RegistryKey>
89
74 - <RegistryKey Key="Instance">
75 - <RegistryValue Name="CLSID" Value="{4FE04BFD-85B9-49DD-B914-F4C9556B9DA6}" Type="string"/>
90 + <RegistryKey Key="Instance">
91 + <RegistryValue Name="CLSID" Value="{4FE04BFD-85B9-49DD-B914-F4C9556B9DA6}" Type="string"/>
92
77 - <RegistryKey Key="InitPropertyBag">
78 - <RegistryValue Name="DisplayType" Value="2" Type="integer"/>
79 - <RegistryValue Name="EnumObjectsTelemetryValue" Value="WSL" Type="string"/>
80 - <RegistryValue Name="Provider" Value="Plan 9 Network Provider" Type="string"/>
81 - <RegistryValue Name="ResName" Value="\\wsl.localhost" Type="string"/>
93 + <RegistryKey Key="InitPropertyBag">
94 + <RegistryValue Name="DisplayType" Value="2" Type="integer"/>
95 + <RegistryValue Name="EnumObjectsTelemetryValue" Value="WSL" Type="string"/>
96 + <RegistryValue Name="Provider" Value="Plan 9 Network Provider" Type="string"/>
97 + <RegistryValue Name="ResName" Value="\\wsl.localhost" Type="string"/>
98 + </RegistryKey>
99 </RegistryKey>
100 </RegistryKey>
84 - </RegistryKey>
101
86 - <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel">
87 - <RegistryValue Name="{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}" Value="1" Type="integer"/>
88 - </RegistryKey>
102 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel">
103 + <RegistryValue Name="{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}" Value="1" Type="integer"/>
104 + </RegistryKey>
105
90 - <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}">
91 - <RegistryValue Value="Linux" Type="string"/>
92 - </RegistryKey>
106 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}">
107 + <RegistryValue Value="Linux" Type="string"/>
108 + </RegistryKey>
109
94 - <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\IdListAliasTranslations\WSL">
95 - <RegistryValue Name="Target" Value="::{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}" Type="string"/>
96 - <RegistryValue Name="Source" Value="\\wsl.localhost" Type="string"/>
97 - </RegistryKey>
110 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\IdListAliasTranslations\WSL">
111 + <RegistryValue Name="Target" Value="::{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}" Type="string"/>
112 + <RegistryValue Name="Source" Value="\\wsl.localhost" Type="string"/>
113 + </RegistryKey>
114
99 - <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\IdListAliasTranslations\WSLLegacy">
100 - <RegistryValue Name="Target" Value="::{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}" Type="string"/>
101 - <RegistryValue Name="Source" Value="\\wsl$" Type="string"/>
102 - </RegistryKey>
115 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\IdListAliasTranslations\WSLLegacy">
116 + <RegistryValue Name="Target" Value="::{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}" Type="string"/>
117 + <RegistryValue Name="Source" Value="\\wsl$" Type="string"/>
118 + </RegistryKey>
119 </Component>
120
121 <Component Id="explorershell" Guid="{93CBFF23-A04C-4344-A332-238CE5B97AEC}" UninstallWhenSuperseded="yes" DisableRegistryReflection="yes" Bitness="always64">
122 <?foreach PATH in SOFTWARE\Classes\Directory\shell\WSL;SOFTWARE\Classes\Directory\Background\shell\WSL;SOFTWARE\Classes\Drive\shell\WSL?>
107 - <RegistryKey Root="HKLM" Key="$(var.PATH)">
108 - <RegistryValue Value="@wsl.exe,-2" Type="string"/>
109 - <RegistryValue Name="Extended" Value="" Type="string"/>
110 - <RegistryValue Name="NoWorkingDirectory" Value="" Type="string"/>
111 - <RegistryKey Key="command">
112 - <RegistryValue Value='wsl.exe --cd "%V"' Type="string"/>
123 + <RegistryKey Root="HKLM" Key="$(var.PATH)">
124 + <RegistryValue Value="@wsl.exe,-2" Type="string"/>
125 + <RegistryValue Name="Extended" Value="" Type="string"/>
126 + <RegistryValue Name="NoWorkingDirectory" Value="" Type="string"/>
127 + <RegistryKey Key="command">
128 + <RegistryValue Value='wsl.exe --cd "%V"' Type="string"/>
129 + </RegistryKey>
130 </RegistryKey>
114 - </RegistryKey>
115 - <?endforeach?>
131 + <?endforeach?>
132
117 - <ProgId Id="WSLDistributionTar" Description="WSL tar distribution" Icon="wsl.exe">
118 - <Extension Id="wsl">
119 - <Verb Id="open" Command="open" TargetFile="wsl.exe" Argument="--install --prompt-before-exit --from-file &quot;%1&quot;" />
120 - </Extension>
121 - </ProgId>
133 + <ProgId Id="WSLDistributionTar" Description="WSL tar distribution" Icon="wsl.exe">
134 + <Extension Id="wsl">
135 + <Verb Id="open" Command="open" TargetFile="wsl.exe" Argument="--install --prompt-before-exit --from-file &quot;%1&quot;" />
136 + </Extension>
137 + </ProgId>
138 </Component>
139
140 <Component Id="wslservice" Guid="F0C8D6BA-1502-41E7-BF72-D93DFA134735" UninstallWhenSuperseded="yes" DisableRegistryReflection="yes" Bitness="always64">
@@ -131,20 +147,21 @@
147 </RegistryKey>
148 </RegistryKey>
149
150 + <!-- WSLServiceProxyStub. -->
151 <RegistryKey Root="HKCR" Key="CLSID\{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}">
152 <RegistryValue Value="PSFactoryBuffer" Type="string" />
136 - </RegistryKey>
137 - <RegistryKey Root="HKCR" Key="CLSID\{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}\InProcServer32">
138 - <RegistryValue Value="[INSTALLDIR]wslserviceproxystub.dll" Type="string" />
139 - <RegistryValue Name="ThreadingModel" Value="Both" Type="string" />
153 + <RegistryKey Key="InProcServer32">
154 + <RegistryValue Value="[INSTALLDIR]wslserviceproxystub.dll" Type="string" />
155 + <RegistryValue Name="ThreadingModel" Value="Both" Type="string" />
156 + </RegistryKey>
157 </RegistryKey>
158
159 <!-- ILxssUserSession -->
160 <RegistryKey Root="HKCR" Key="AppID\{370121D2-AA7E-4608-A86D-0BBAB9DA1A60}">
161
162 <!-- O:BAG:BAD:(A;;CCDCSW;;;AU)(A;;CCDCSW;;;PS)(A;;CCDCSW;;;SY) -->
146 - <RegistryValue Name="AccessPermission" Value="01000480580000006800000000000000140000000200440003000000000014000B00000001010000000000050B000000000014000B00000001010000000000050A000000000014000B0000000101000000000005120000000102000000000005200000002002000001020000000000052000000020020000" Type="binary" />
147 - <RegistryValue Name="LaunchPermission" Value="01000480580000006800000000000000140000000200440003000000000014000B00000001010000000000050B000000000014000B00000001010000000000050A000000000014000B0000000101000000000005120000000102000000000005200000002002000001020000000000052000000020020000" Type="binary" />
163 + <RegistryValue Name="AccessPermission" Value="${DCOM_PERMISSION}" Type="binary" />
164 + <RegistryValue Name="LaunchPermission" Value="${DCOM_PERMISSION}" Type="binary" />
165 <RegistryValue Name="LocalService" Value="WSLService" Type="string" />
166 </RegistryKey>
167
@@ -165,8 +182,8 @@
182 <RegistryValue Name="AppIDFlags" Value="2048" Type="integer" />
183
184 <!-- O:BAG:BAD:(A;;CCDCSW;;;AU)(A;;CCDCSW;;;PS)(A;;CCDCSW;;;SY) -->
168 - <RegistryValue Name="AccessPermission" Value="01000480580000006800000000000000140000000200440003000000000014000B00000001010000000000050B000000000014000B00000001010000000000050A000000000014000B0000000101000000000005120000000102000000000005200000002002000001020000000000052000000020020000" Type="binary" />
169 - <RegistryValue Name="LaunchPermission" Value="01000480580000006800000000000000140000000200440003000000000014000B00000001010000000000050B000000000014000B00000001010000000000050A000000000014000B0000000101000000000005120000000102000000000005200000002002000001020000000000052000000020020000" Type="binary" />
185 + <RegistryValue Name="AccessPermission" Value="${DCOM_PERMISSION}" Type="binary" />
186 + <RegistryValue Name="LaunchPermission" Value="${DCOM_PERMISSION}" Type="binary" />
187 </RegistryKey>
188
189 <!-- WslDeviceHost_VirtioPmem -->
@@ -223,8 +240,121 @@
240 <ServiceControl Id="StopService" Stop="both" Remove="uninstall" Name="WSLService" Wait="yes" />
241
242 <File Id="wsldevicehost.dll" Source="${WSL_DEVICE_HOST_SOURCE_DIR}/bin/${TARGET_PLATFORM}/wsldevicehost.dll" />
226 - </Component>
243
244 + <!-- WSLC COM app - activated through WSLService -->
245 + <RegistryKey Root="HKCR" Key="AppID\{E9B79997-57E3-4201-AECC-6A464E530DD2}">
246 + <!-- O:BAG:BAD:(A;;CCDCSW;;;AU)(A;;CCDCSW;;;PS)(A;;CCDCSW;;;SY) -->
247 + <RegistryValue Name="AccessPermission" Value="${DCOM_PERMISSION}" Type="binary" />
248 + <RegistryValue Name="LaunchPermission" Value="${DCOM_PERMISSION}" Type="binary" />
249 + <RegistryValue Name="LocalService" Value="WSLService" Type="string" />
250 + </RegistryKey>
251 +
252 + <!-- WSLCContainer -->
253 + <RegistryKey Root="HKCR" Key="CLSID\{B1F1C4E3-C225-4CAE-AD8A-34C004DE1AE4}">
254 + <RegistryValue Name="AppId" Value="{E9B79997-57E3-4201-AECC-6A464E530DD2}" Type="string" />
255 + <RegistryValue Value="WSLCContainer" Type="string" />
256 + </RegistryKey>
257 +
258 + <!-- WSLCProcess -->
259 + <RegistryKey Root="HKCR" Key="CLSID\{AFBEA6D6-D8A4-4F81-8FED-F947EB74B33B}">
260 + <RegistryValue Name="AppId" Value="{E9B79997-57E3-4201-AECC-6A464E530DD2}" Type="string" />
261 + <RegistryValue Value="WSLCProcess" Type="string" />
262 + </RegistryKey>
263 +
264 + <!-- WSLCSessionManager -->
265 + <RegistryKey Root="HKCR" Key="CLSID\{a9b7a1b9-0671-405c-95f1-e0612cb4ce8f}">
266 + <RegistryValue Name="AppId" Value="{E9B79997-57E3-4201-AECC-6A464E530DD2}" Type="string" />
267 + <RegistryValue Value="WSLCSessionManager" Type="string" />
268 + </RegistryKey>
269 +
270 + <!-- WSLCSessionFactory - COM server in per-user process -->
271 + <RegistryKey Root="HKCR" Key="AppID\{1FAB86C3-F4DF-4271-8E63-6F071C4F708A}">
272 + <!-- O:BAG:BAD:(A;;CCDCSW;;;AU)(A;;CCDCSW;;;PS)(A;;CCDCSW;;;SY) -->
273 + <RegistryValue Name="AccessPermission" Value="${DCOM_PERMISSION}" Type="binary" />
274 + <RegistryValue Name="LaunchPermission" Value="${DCOM_PERMISSION}" Type="binary" />
275 + </RegistryKey>
276 + <RegistryKey Root="HKCR" Key="CLSID\{9FCD2067-9FC6-4EFA-9EB0-698169EBF7D3}">
277 + <RegistryValue Name="AppId" Value="{1FAB86C3-F4DF-4271-8E63-6F071C4F708A}" Type="string" />
278 + <RegistryValue Value="WSLCSessionFactory" Type="string" />
279 + <RegistryKey Key="LocalServer32">
280 + <RegistryValue Value='"[INSTALLDIR]wslcsession.exe"' Type="string" />
281 + </RegistryKey>
282 + </RegistryKey>
283 +
284 + <!-- IWSLCSessionManager-->
285 + <RegistryKey Root="HKCR" Key="Interface\{82A7ABC8-6B50-43FC-AB96-15FBBE7E8760}">
286 + <RegistryValue Value="IWSLCSessionManager" Type="string" />
287 + <RegistryKey Key="ProxyStubClsid32">
288 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
289 + </RegistryKey>
290 + </RegistryKey>
291 +
292 + <!-- IWSLCContainer-->
293 + <RegistryKey Root="HKCR" Key="Interface\{7577FE8D-DE85-471E-B870-11669986F332}">
294 + <RegistryValue Value="IWSLCContainer" Type="string" />
295 + <RegistryKey Key="ProxyStubClsid32">
296 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
297 + </RegistryKey>
298 + </RegistryKey>
299 +
300 + <!-- IWSLCProcess-->
301 + <RegistryKey Root="HKCR" Key="Interface\{1AD163CD-393D-4B33-83A2-8A3F3F23E608}">
302 + <RegistryValue Value="IWSLCProcess" Type="string" />
303 + <RegistryKey Key="ProxyStubClsid32">
304 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
305 + </RegistryKey>
306 + </RegistryKey>
307 +
308 + <!-- ITerminationCallback-->
309 + <RegistryKey Root="HKCR" Key="Interface\{7BC4E198-6531-4FA6-ADE2-5EF3D2A04DFE}">
310 + <RegistryValue Value="ITerminationCallback" Type="string" />
311 + <RegistryKey Key="ProxyStubClsid32">
312 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
313 + </RegistryKey>
314 + </RegistryKey>
315 +
316 + <!-- IProgressCallback-->
317 + <RegistryKey Root="HKCR" Key="Interface\{5038842F-53DB-4F30-A6D0-A41B02C94AC1}">
318 + <RegistryValue Value="IProgressCallback" Type="string" />
319 + <RegistryKey Key="ProxyStubClsid32">
320 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
321 + </RegistryKey>
322 + </RegistryKey>
323 +
324 + <!-- IWSLCSession-->
325 + <RegistryKey Root="HKCR" Key="Interface\{EF0661E4-6364-40EA-B433-E2FDF11F3519}">
326 + <RegistryValue Value="IWSLCSession" Type="string" />
327 + <RegistryKey Key="ProxyStubClsid32">
328 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
329 + </RegistryKey>
330 + </RegistryKey>
331 +
332 + <!-- IWSLCSessionReference-->
333 + <RegistryKey Root="HKCR" Key="Interface\{B3A72F48-9D15-4E8A-A621-7C3E84F09B52}">
334 + <RegistryValue Value="IWSLCSessionReference" Type="string" />
335 + <RegistryKey Key="ProxyStubClsid32">
336 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
337 + </RegistryKey>
338 + </RegistryKey>
339 +
340 + <!-- IWSLCSessionFactory-->
341 + <RegistryKey Root="HKCR" Key="Interface\{C4E8F291-3B5D-4A7C-9E12-8F6A4D2B7C91}">
342 + <RegistryValue Value="IWSLCSessionFactory" Type="string" />
343 + <RegistryKey Key="ProxyStubClsid32">
344 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
345 + </RegistryKey>
346 + </RegistryKey>
347 +
348 + <!-- IWSLCVirtualMachine-->
349 + <RegistryKey Root="HKCR" Key="Interface\{B5E2D8F1-9A3C-4E6B-8D1F-7C4A2E9B6D3A}">
350 + <RegistryValue Value="IWSLCVirtualMachine" Type="string" />
351 + <RegistryKey Key="ProxyStubClsid32">
352 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
353 + </RegistryKey>
354 + </RegistryKey>
355 +
356 + <File Id="wslcsession.exe" Source="${BIN}/wslcsession.exe" />
357 + </Component>
358 <Component Id="wslg" Guid="F0C8D6BA-1502-41E7-BF72-D93DFA134731" UninstallWhenSuperseded="yes" DisableRegistryReflection="yes" Bitness="always64">
359 <?if "${WSL_DEV_BINARY_PATH}" = "" ?>
360 <File Id="msrdc.exe" Source="${MSRDC_SOURCE_DIR}/${TARGET_PLATFORM}/msrdc.exe" />
@@ -464,7 +594,7 @@
594 />
595
596
467 - <CustomAction Id="RemoveRegistryKeyProtections"
597 + <CustomAction Id="RemoveRegistryKeyProtections"
598 Impersonate="no"
599 BinaryRef="wslinstall.dll"
600 DllEntry="RemoveRegistryKeyProtections"
@@ -472,7 +602,7 @@
602 Execute="deferred"
603 />
604
475 - <CustomAction Id="UnregisterLspCategories"
605 + <CustomAction Id="UnregisterLspCategories"
606 Impersonate="no"
607 BinaryRef="wslinstall.dll"
608 DllEntry="UnregisterLspCategories"
nuget.config
-1
@@ -26,4 +26,3 @@
26 <clear />
27 </disabledPackageSources>
28 </configuration>
29 -
nuget/CMakeLists.txt
+2 -1
@@ -1,4 +1,5 @@
1 -set(NUGET_PACKAGES Microsoft.WSL.PluginApi.nuspec)
1 +set(NUGET_PACKAGES Microsoft.WSL.PluginApi.nuspec Microsoft.WSL.Containers.nuspec)
2 +set(WSL_NUGET_TARGET_FRAMEWORK "net8.0-windows10.0.19041.0")
3
4 # generate vars with native paths since nuget won't accept unix path separators
5 cmake_path(NATIVE_PATH CMAKE_SOURCE_DIR CMAKE_SOURCE_DIR_NATIVE)
nuget/Microsoft.WSL.Containers.nuspec.in new
+28
@@ -0,0 +1,28 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
3 + <metadata>
4 + <id>Microsoft.WSL.Containers</id>
5 + <version>${WSL_NUGET_PACKAGE_VERSION}</version>
6 + <authors>Microsoft</authors>
7 + <projectUrl>https://github.com/microsoft/WSL</projectUrl>
8 + <description>WSL Containers SDK (Preview - API subject to breaking changes)</description>
9 + <copyright>© Microsoft Corporation. All rights reserved.</copyright>
10 + <tags>WSL</tags>
11 + <language>en-us</language>
12 + <license type="expression">MIT</license>
13 + <readme>docs\README.MD</readme>
14 + <dependencies>
15 + <group targetFramework="${WSL_NUGET_TARGET_FRAMEWORK}" />
16 + </dependencies>
17 + </metadata>
18 + <files>
19 + <file src="${CMAKE_SOURCE_DIR_NATIVE}\src\windows\WslcSDK\wslcsdk.h" target="include"/>
20 + <file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\x64\Release\wslcsdkcs.dll" target="lib\${WSL_NUGET_TARGET_FRAMEWORK}"/>
21 + <file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\x64\Release\wslcsdk.lib" target="runtimes\win-x64"/>
22 + <file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\x64\Release\wslcsdk.dll" target="runtimes\win-x64\native"/>
23 + <file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\arm64\Release\wslcsdk.lib" target="runtimes\win-arm64"/>
24 + <file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\arm64\Release\wslcsdk.dll" target="runtimes\win-arm64\native"/>
25 + <file src="${CMAKE_SOURCE_DIR_NATIVE}\nuget\Microsoft.WSL.Containers\**" exclude="**\net\**"/>
26 + <file src="${CMAKE_SOURCE_DIR_NATIVE}\nuget\Microsoft.WSL.Containers\build\net\**" target="build\${WSL_NUGET_TARGET_FRAMEWORK}"/>
27 + </files>
28 +</package>
nuget/Microsoft.WSL.Containers/build/native/Microsoft.WSL.Containers.targets new
+34
@@ -0,0 +1,34 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3 + <PropertyGroup>
4 + <WslcPlatform Condition="'$(WslcPlatform)' == ''">$(Platform)</WslcPlatform>
5 + <_wslcIsInvalidPlatform Condition="'$(WslcPlatform)' != 'x64' and '$(WslcPlatform)' != 'arm64'">true</_wslcIsInvalidPlatform>
6 + </PropertyGroup>
7 +
8 + <ItemDefinitionGroup>
9 + <ClCompile>
10 + <AdditionalIncludeDirectories>
11 + $(MSBuildThisFileDirectory)..\..\include;
12 + %(AdditionalIncludeDirectories)
13 + </AdditionalIncludeDirectories>
14 + </ClCompile>
15 + <Link>
16 + <AdditionalDependencies>
17 + wslcsdk.lib;
18 + %(AdditionalDependencies)
19 + </AdditionalDependencies>
20 + <AdditionalLibraryDirectories>
21 + $(MSBuildThisFileDirectory)..\..\runtimes\win-$(WslcPlatform);
22 + %(AdditionalLibraryDirectories)
23 + </AdditionalLibraryDirectories>
24 + </Link>
25 + </ItemDefinitionGroup>
26 +
27 + <ItemGroup>
28 + <ReferenceCopyLocalPaths Include="$(MSBuildThisFileDirectory)..\..\runtimes\win-$(WslcPlatform)\native\wslcsdk.dll" />
29 + </ItemGroup>
30 +
31 + <Target Name="WslcValidatePlatform" BeforeTargets="PrepareForBuild" Condition="'$(_wslcIsInvalidPlatform)' == 'true'">
32 + <Error Text="wslcsdk.dll could not be copied because platform '$(WslcPlatform)' is not supported. Only x64 and arm64 platforms are supported. You can override the detected platform by setting the property WslcPlatform." />
33 + </Target>
34 +</Project>
\ No newline at end of file
nuget/Microsoft.WSL.Containers/build/net/Microsoft.WSL.Containers.targets new
+31
@@ -0,0 +1,31 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3 + <!-- If we have a RuntimeIdentifier, the DLL is referenced automatically.
4 + If it is missing, we fall back to PlatformTarget to reference it manually. -->
5 + <Choose>
6 + <When Condition="'$(RuntimeIdentifier)' != ''">
7 + <PropertyGroup>
8 + <_wslcPlatform Condition="$(RuntimeIdentifier.EndsWith('-x64'))">x64</_wslcPlatform>
9 + <_wslcPlatform Condition="$(RuntimeIdentifier.EndsWith('-arm64'))">arm64</_wslcPlatform>
10 + <_wslcInvalidPlatformProperty Condition="'$(_wslcPlatform)' == ''">RuntimeIdentifier</_wslcInvalidPlatformProperty>
11 + <_wslcInvalidPlatform Condition="'$(_wslcPlatform)' == ''">$(RuntimeIdentifier)</_wslcInvalidPlatform>
12 + </PropertyGroup>
13 + </When>
14 + <Otherwise>
15 + <PropertyGroup>
16 + <_wslcPlatform Condition="'$(PlatformTarget)' == 'x64'">x64</_wslcPlatform>
17 + <_wslcPlatform Condition="'$(PlatformTarget)' == 'arm64'">arm64</_wslcPlatform>
18 + <_wslcInvalidPlatformProperty Condition="'$(_wslcPlatform)' == ''">PlatformTarget</_wslcInvalidPlatformProperty>
19 + <_wslcInvalidPlatform Condition="'$(_wslcPlatform)' == ''">$(PlatformTarget)</_wslcInvalidPlatform>
20 + </PropertyGroup>
21 +
22 + <ItemGroup Condition="'$(_wslcPlatform)' != ''">
23 + <ReferenceCopyLocalPaths Include="$(MSBuildThisFileDirectory)..\..\runtimes\win-$(_wslcPlatform)\native\wslcsdk.dll" />
24 + </ItemGroup>
25 + </Otherwise>
26 + </Choose>
27 +
28 + <Target Name="WslcValidatePlatform" BeforeTargets="PrepareForBuild" Condition="'$(_wslcInvalidPlatform)' != ''">
29 + <Error Text="wslcsdk.dll could not be copied because the $(_wslcInvalidPlatformProperty) '$(_wslcInvalidPlatform)' is not supported. Only x64 and arm64 platforms are supported." />
30 + </Target>
31 +</Project>
\ No newline at end of file
nuget/Microsoft.WSL.Containers/cmake/Microsoft.WSL.ContainersConfig.cmake new
+57
@@ -0,0 +1,57 @@
1 +if(TARGET Microsoft.WSL.Containers::SDK)
2 + return()
3 +endif()
4 +
5 +if(NOT WIN32)
6 + message(FATAL_ERROR "Microsoft.WSL.Containers: This package only supports Windows.")
7 +endif()
8 +
9 +# Determine target architecture
10 +if(CMAKE_GENERATOR_PLATFORM)
11 + string(TOLOWER "${CMAKE_GENERATOR_PLATFORM}" _wslcsdk_platform)
12 + if(_wslcsdk_platform STREQUAL "x64")
13 + set(_wslcsdk_arch "x64")
14 + elseif(_wslcsdk_platform STREQUAL "arm64")
15 + set(_wslcsdk_arch "arm64")
16 + else()
17 + message(FATAL_ERROR
18 + "Microsoft.WSL.Containers: Unsupported platform '${CMAKE_GENERATOR_PLATFORM}'."
19 + " Supported: x64, ARM64.")
20 + endif()
21 + unset(_wslcsdk_platform)
22 +elseif(CMAKE_SYSTEM_PROCESSOR)
23 + string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _wslcsdk_platform)
24 + if(_wslcsdk_platform MATCHES "amd64|x86_64|x64")
25 + set(_wslcsdk_arch "x64")
26 + elseif(_wslcsdk_platform MATCHES "arm64|aarch64")
27 + set(_wslcsdk_arch "arm64")
28 + else()
29 + message(FATAL_ERROR
30 + "Microsoft.WSL.Containers: Unsupported architecture '${CMAKE_SYSTEM_PROCESSOR}'."
31 + " Supported: x64, ARM64.")
32 + endif()
33 + unset(_wslcsdk_platform)
34 +else()
35 + message(FATAL_ERROR
36 + "Microsoft.WSL.Containers: Could not determine target architecture."
37 + " Set CMAKE_GENERATOR_PLATFORM or CMAKE_SYSTEM_PROCESSOR.")
38 +endif()
39 +
40 +# Compute paths relative to package root (<root>/cmake/ -> <root>/)
41 +get_filename_component(_wslcsdk_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
42 +set(_wslcsdk_include_dir "${_wslcsdk_root}/include")
43 +set(_wslcsdk_lib_dir "${_wslcsdk_root}/runtimes/win-${_wslcsdk_arch}")
44 +
45 +# Create imported target
46 +add_library(Microsoft.WSL.Containers::SDK SHARED IMPORTED GLOBAL)
47 +set_target_properties(Microsoft.WSL.Containers::SDK PROPERTIES
48 + INTERFACE_INCLUDE_DIRECTORIES "${_wslcsdk_include_dir}"
49 + IMPORTED_IMPLIB "${_wslcsdk_lib_dir}/wslcsdk.lib"
50 + IMPORTED_LOCATION "${_wslcsdk_lib_dir}/native/wslcsdk.dll"
51 +)
52 +
53 +# Clean up temporary variables
54 +unset(_wslcsdk_arch)
55 +unset(_wslcsdk_root)
56 +unset(_wslcsdk_include_dir)
57 +unset(_wslcsdk_lib_dir)
nuget/Microsoft.WSL.Containers/docs/README.MD new
+7
@@ -0,0 +1,7 @@
1 +# WSL Containers
2 +
3 +> **⚠️ Preview:** This SDK is currently in preview and is subject to breaking changes
4 +> in future releases without prior notice. Do not rely on API stability for production
5 +> workloads.
6 +
7 +This package contains the `wslcsdk.h` header which defines the WSL Containers interface.
nuget/Microsoft.WSL.PluginApi.nuspec.in
+1 -1
@@ -16,6 +16,6 @@
16 <files>
17 <file src="${CMAKE_SOURCE_DIR_NATIVE}\src\windows\inc\WslPluginApi.h" target="build\native\include"/>
18 <file src="${CMAKE_SOURCE_DIR_NATIVE}\Images\Square44x44Logo.altform-lightunplated_targetsize-256.png" target="images\icon.png"/>
19 - <file src="${CMAKE_SOURCE_DIR_NATIVE}\nuget\README.WslPluginApi.MD" target="docs\README.MD"/>
19 + <file src="${CMAKE_SOURCE_DIR_NATIVE}\nuget\Microsoft.WSL.PluginApi\**"/>
20 </files>
21 </package>
nuget/Microsoft.WSL.PluginApi/docs/README.MD renamed
packages.config
+3 -2
@@ -1,4 +1,4 @@
1 -<?xml version="1.0" encoding="utf-8"?>
1 +<?xml version="1.0" encoding="utf-8"?>
2 <packages>
3 <package id="CommunityToolkit.Mvvm" version="8.4.0" />
4 <package id="CommunityToolkit.WinUI.Animations" version="8.2.250402" />
@@ -21,8 +21,9 @@
21 <package id="Microsoft.WSL.DeviceHost" version="1.2.14-0" />
22 <package id="Microsoft.WSL.Kernel" version="6.6.114.1-1" targetFramework="native" />
23 <package id="Microsoft.WSL.LinuxSdk" version="1.20.0" targetFramework="native" />
24 + <package id="Microsoft.WSL.TestData" version="0.4.0" />
25 <package id="Microsoft.WSL.TestDistro" version="2.7.1-1" />
25 - <package id="Microsoft.WSLg" version="1.0.73" />
26 + <package id="Microsoft.WSLg" version="1.0.76" />
27 <package id="Microsoft.Xaml.Behaviors.WinUI.Managed" version="3.0.0" />
28 <package id="vswhere" version="3.1.7" />
29 <package id="WinUIEx" version="2.9.0" />
src/linux/inc/lxwil.h
+5
@@ -373,6 +373,11 @@ public:
373 return fd;
374 }
375
376 + int* addressof() noexcept
377 + {
378 + return &m_Fd;
379 + }
380 +
381 friend void swap(unique_fd& fd1, unique_fd& fd2)
382 {
383 std::swap(fd1.m_Fd, fd2.m_Fd);
src/linux/init/CMakeLists.txt
+2 -1
@@ -20,7 +20,8 @@ set(SOURCES
20 util.cpp
21 WslDistributionConfig.cpp
22 wslinfo.cpp
23 - wslpath.cpp)
23 + wslpath.cpp
24 + WSLCInit.cpp)
25
26 set(HEADERS
27 ../inc/lxwil.h
src/linux/init/GnsEngine.cpp
+1
@@ -328,6 +328,7 @@ void GnsEngine::ProcessDNSChange(Interface& interface, const wsl::shared::hns::D
328 GNS_LOG_INFO(
329 "Setting DNS search to {}: {} on interfaceName {} ", payload.Search.c_str(), content.str().c_str(), interface.Name().c_str());
330
331 + THROW_LAST_ERROR_IF(UtilMkdirPath("/etc", 0755) < 0);
332 std::wofstream resolvConf;
333 resolvConf.exceptions(std::ofstream::badbit | std::ofstream::failbit);
334 resolvConf.open("/etc/resolv.conf", std::ofstream::trunc);
src/linux/init/WSLCInit.cpp new
+1127
@@ -0,0 +1,1127 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCInit.cpp
8 +
9 +Abstract:
10 +
11 + Init implementation for WSLC.
12 +
13 +--*/
14 +
15 +#include "util.h"
16 +#include "SocketChannel.h"
17 +#include "message.h"
18 +#include "localhost.h"
19 +#include "common.h"
20 +#include <utmp.h>
21 +#include <unistd.h>
22 +#include <sys/wait.h>
23 +#include <sys/mount.h>
24 +#include <sys/syscall.h>
25 +#include <sys/epoll.h>
26 +#include <sys/prctl.h>
27 +#include <sys/socket.h>
28 +#include <sys/utsname.h>
29 +#include <sys/signalfd.h>
30 +#include <arpa/inet.h>
31 +
32 +#include <pty.h>
33 +#include "mountutilcpp.h"
34 +#include <filesystem>
35 +
36 +extern int InitializeLogging(bool SetStderr, wil::LogFunction* ExceptionCallback) noexcept;
37 +
38 +extern std::set<pid_t> ListInitChildProcesses();
39 +
40 +extern std::vector<unsigned int> ListScsiDisks();
41 +
42 +extern int DetachScsiDisk(unsigned int Lun);
43 +
44 +extern std::string GetLunDeviceName(unsigned int Lun);
45 +
46 +void ProcessMessages(wsl::shared::SocketChannel& Channel);
47 +int MountInit(const char* Target);
48 +
49 +extern int EnableInterface(int Socket, const char* Name);
50 +
51 +extern int SetCloseOnExec(int Fd, bool Enable);
52 +
53 +int Chroot(const char* Target);
54 +
55 +extern int g_LogFd;
56 +
57 +extern void WSLCEnableCrashDumpCollection();
58 +
59 +struct WSLCState
60 +{
61 + std::optional<std::filesystem::path> ModulesMountPoint;
62 +};
63 +
64 +static WSLCState g_state;
65 +
66 +int CreateCaptureCrashSymlink()
67 +try
68 +{
69 + THROW_LAST_ERROR_IF(symlink("/init", "/" LX_INIT_WSL_CAPTURE_CRASH) < 0);
70 +
71 + return 0;
72 +}
73 +CATCH_RETURN_ERRNO()
74 +
75 +void WSLCEnableCrashDumpCollection()
76 +{
77 + if (CreateCaptureCrashSymlink() < 0)
78 + {
79 + return;
80 + }
81 +
82 + // If the first character is a pipe, then the kernel will interpret this path as a command.
83 + constexpr auto core_pattern = "|/" LX_INIT_WSL_CAPTURE_CRASH " %t %E %p %s";
84 + WriteToFile("/proc/sys/kernel/core_pattern", core_pattern);
85 +}
86 +
87 +void HandleMessageImpl(
88 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_GET_DISK& Message, const gsl::span<gsl::byte>& Buffer)
89 +{
90 + wsl::shared::MessageWriter<WSLC_GET_DISK_RESULT> writer;
91 +
92 + try
93 + {
94 + auto deviceName = GetLunDeviceName(Message.ScsiLun);
95 +
96 + writer->Result = 0;
97 + writer.WriteString("/dev/" + deviceName);
98 + }
99 + catch (...)
100 + {
101 + writer->Result = wil::ResultFromCaughtException();
102 + }
103 +
104 + Transaction.Send<WSLC_GET_DISK::TResponse>(writer.Span());
105 +}
106 +
107 +void HandleMessageImpl(
108 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_ACCEPT& Message, const gsl::span<gsl::byte>& Buffer)
109 +{
110 + sockaddr_vm SocketAddress{};
111 + wil::unique_fd ListenSocket{UtilListenVsockAnyPort(&SocketAddress, 1, true)};
112 + THROW_LAST_ERROR_IF(!ListenSocket);
113 +
114 + Transaction.SendResultMessage<uint32_t>(SocketAddress.svm_port);
115 +
116 + wil::unique_fd Socket{
117 + UtilAcceptVsock(ListenSocket.get(), SocketAddress, SESSION_LEADER_ACCEPT_TIMEOUT_MS, Message.Fd != -1 ? SOCK_CLOEXEC : 0)};
118 + THROW_LAST_ERROR_IF(!Socket);
119 +
120 + if (Message.Fd != -1)
121 + {
122 + THROW_LAST_ERROR_IF(dup2(Socket.get(), Message.Fd) < 0);
123 + }
124 + else
125 + {
126 + Transaction.SendResultMessage<int32_t>(Socket.get());
127 + Socket.release();
128 + }
129 +}
130 +
131 +void HandleMessageImpl(
132 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_CONNECT& Message, const gsl::span<gsl::byte>& Buffer)
133 +{
134 + int32_t result = -EINVAL;
135 + auto sendResult = wil::scope_exit([&]() { Transaction.SendResultMessage(result); });
136 +
137 + auto fd = UtilConnectVsock(Message.HostPort, true);
138 + if (!fd)
139 + {
140 + result = -errno;
141 + }
142 + else
143 + {
144 + result = fd.release();
145 + }
146 +}
147 +
148 +void HandleMessageImpl(
149 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_UNIX_CONNECT& Message, const gsl::span<gsl::byte>& Buffer)
150 +{
151 + // Make sure to close the channel since no more messages can be processed after this.
152 + auto closeChannel = wil::scope_exit([&]() { Channel.Close(); });
153 +
154 + int result = -1;
155 + auto sendResult = wil::scope_exit([&]() { Transaction.SendResultMessage(result); });
156 +
157 + const auto* path = wsl::shared::string::FromSpan(Buffer, Message.PathOffset);
158 + THROW_ERRNO_IF(EINVAL, path == nullptr);
159 +
160 + wil::unique_fd socket;
161 +
162 + try
163 + {
164 + socket = UtilConnectUnix(path);
165 + result = 0;
166 + }
167 + catch (...)
168 + {
169 + result = wil::ResultFromCaughtException();
170 + }
171 +
172 + if (result != 0)
173 + {
174 + return;
175 + }
176 +
177 + sendResult.reset();
178 +
179 + // Relay data between the two sockets.
180 + pollfd pollDescriptors[2];
181 + pollDescriptors[0].fd = socket.get();
182 + pollDescriptors[0].events = POLLIN;
183 + pollDescriptors[1].fd = Channel.Socket();
184 + pollDescriptors[1].events = POLLIN;
185 +
186 + std::vector<gsl::byte> relayBuffer;
187 + while (true)
188 + {
189 + auto result = poll(pollDescriptors, COUNT_OF(pollDescriptors), -1);
190 + THROW_LAST_ERROR_IF(result < 0);
191 +
192 + if (pollDescriptors[0].revents & (POLLIN | POLLHUP | POLLERR))
193 + {
194 + auto bytesRead = UtilReadBuffer(pollDescriptors[0].fd, relayBuffer);
195 + if (bytesRead < 0)
196 + {
197 + LOG_ERROR("read failed {}", errno);
198 + break;
199 + }
200 + else if (bytesRead == 0)
201 + {
202 + // Unix socket has been closed. Gracefully half-close the
203 + // hvsocket so the Windows side receives a clean EOF instead
204 + // of ERROR_BROKEN_PIPE.
205 + pollDescriptors[0].fd = -1;
206 + if (shutdown(Channel.Socket(), SHUT_WR) < 0)
207 + {
208 + LOG_ERROR("shutdown({}, SHUT_WR) failed {}", Channel.Socket(), errno);
209 + }
210 +
211 + break;
212 + }
213 + else if (UtilWriteBuffer(Channel.Socket(), relayBuffer.data(), bytesRead) < 0)
214 + {
215 + LOG_ERROR("write failed {}", errno);
216 + break;
217 + }
218 + }
219 +
220 + if (pollDescriptors[1].revents & (POLLIN | POLLHUP | POLLERR))
221 + {
222 + auto bytesRead = UtilReadBuffer(pollDescriptors[1].fd, relayBuffer);
223 + if (bytesRead < 0)
224 + {
225 + LOG_ERROR("read failed {}", errno);
226 + break;
227 + }
228 + else if (bytesRead == 0)
229 + {
230 + // hvsocket has been closed.
231 + pollDescriptors[1].fd = -1;
232 +
233 + // Shutdown the write side of the socket. This is required so docker knows when stdin is in EOF for instance.
234 + if (shutdown(socket.get(), SHUT_WR) < 0)
235 + {
236 + LOG_ERROR("shutdown({}, SHUT_WR) failed {}", socket.get(), errno);
237 + }
238 + }
239 + else if (UtilWriteBuffer(socket.get(), relayBuffer.data(), bytesRead) < 0)
240 + {
241 + LOG_ERROR("write failed {}", errno);
242 + break;
243 + }
244 + }
245 + }
246 +}
247 +
248 +void HandleMessageImpl(
249 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_TTY_RELAY& Message, const gsl::span<gsl::byte>&)
250 +{
251 + THROW_LAST_ERROR_IF(fcntl(Message.TtyMaster, F_SETFL, O_NONBLOCK) < 0);
252 +
253 + wsl::shared::SocketChannel TerminalControlChannel({Message.TtyControl}, "TerminalControl");
254 +
255 + pollfd pollDescriptors[3];
256 +
257 + pollDescriptors[0].fd = Message.Socket;
258 + pollDescriptors[0].events = POLLIN;
259 + pollDescriptors[1].fd = Message.TtyMaster;
260 + pollDescriptors[1].events = POLLIN;
261 + pollDescriptors[2].fd = Message.TtyControl;
262 + pollDescriptors[2].events = POLLIN;
263 +
264 + std::vector<gsl::byte> pendingStdin;
265 + std::vector<gsl::byte> buffer;
266 +
267 + Channel.Close();
268 +
269 + while (true)
270 + {
271 + ssize_t bytesWritten = 0;
272 + auto result = poll(pollDescriptors, COUNT_OF(pollDescriptors), pendingStdin.empty() ? -1 : 100);
273 + if (!pendingStdin.empty())
274 + {
275 + bytesWritten = write(Message.TtyMaster, pendingStdin.data(), pendingStdin.size());
276 + if (bytesWritten < 0)
277 + {
278 + if (errno != EAGAIN && errno != EWOULDBLOCK)
279 + {
280 + LOG_ERROR("delayed stdin write failed {}", errno);
281 + }
282 + }
283 + else
284 + {
285 + WI_ASSERT(static_cast<size_t>(bytesWritten) <= pendingStdin.size());
286 +
287 + pendingStdin.erase(pendingStdin.begin(), pendingStdin.begin() + bytesWritten);
288 + }
289 + }
290 +
291 + if (result < 0)
292 + {
293 + LOG_ERROR("poll failed {}", errno);
294 + break;
295 + }
296 +
297 + // Relay stdin.
298 + if (pollDescriptors[0].revents & (POLLIN | POLLHUP | POLLERR) && pendingStdin.empty())
299 + {
300 + auto bytesRead = UtilReadBuffer(pollDescriptors[0].fd, buffer);
301 + if (bytesRead < 0)
302 + {
303 + LOG_ERROR("read failed {}", errno);
304 + break;
305 + }
306 + else if (bytesRead == 0)
307 + { // Stdin has been closed.
308 + pollDescriptors[0].fd = -1;
309 +
310 + CLOSE(Message.TtyMaster);
311 + }
312 + else
313 + {
314 + bytesWritten = write(Message.TtyMaster, buffer.data(), bytesRead);
315 + if (bytesWritten < 0)
316 + {
317 + //
318 + // If writing on stdin's pipe would block, mark the write as pending and continue.
319 + // This is required because blocking on the write() could lead to a deadlock if the child process
320 + // is blocking trying to write on stderr / stdout while the relay tries to write stdin.
321 + //
322 +
323 + if (errno == EWOULDBLOCK || errno == EAGAIN)
324 + {
325 + assert(pendingStdin.empty());
326 + pendingStdin.assign(buffer.begin(), buffer.begin() + bytesRead);
327 + }
328 + else
329 + {
330 + LOG_ERROR("write failed {}", errno);
331 + break;
332 + }
333 + }
334 + else if (bytesWritten < bytesRead)
335 + {
336 + // Partial write — buffer the remaining bytes for the next iteration.
337 + pendingStdin.assign(buffer.begin() + bytesWritten, buffer.begin() + bytesRead);
338 + }
339 + }
340 + }
341 +
342 + // Relay stdout & stderr
343 + if (pollDescriptors[1].revents & (POLLIN | POLLHUP | POLLERR))
344 + {
345 + auto bytesRead = UtilReadBuffer(pollDescriptors[1].fd, buffer);
346 + if (bytesRead <= 0)
347 + {
348 + if (bytesRead < 0 && errno != EIO)
349 + {
350 + LOG_ERROR("read failed {} {}", bytesRead, errno);
351 + }
352 +
353 + // The tty has been closed, stop relaying.
354 + CLOSE(pollDescriptors[1].fd);
355 + pollDescriptors[1].fd = -1;
356 + break;
357 + }
358 +
359 + bytesWritten = UtilWriteBuffer(Message.Socket, buffer.data(), bytesRead);
360 + if (bytesWritten < 0)
361 + {
362 + LOG_ERROR("write failed {}", errno);
363 + CLOSE(pollDescriptors[1].fd);
364 + pollDescriptors[1].fd = -1;
365 + }
366 + }
367 +
368 + // Process message from the terminal control channel.
369 + if (pollDescriptors[2].revents & (POLLIN | POLLHUP | POLLERR))
370 + {
371 + auto [ttyMessage, _] = TerminalControlChannel.ReceiveMessageOrClosed<WSLC_TERMINAL_CHANGED>();
372 +
373 + //
374 + // A zero-byte read means that the control channel has been closed
375 + // and that the relay process should exit.
376 + //
377 +
378 + if (ttyMessage == nullptr)
379 + {
380 + break;
381 + }
382 +
383 + winsize terminal{};
384 + terminal.ws_col = ttyMessage->Columns;
385 + terminal.ws_row = ttyMessage->Rows;
386 + if (ioctl(Message.TtyMaster, TIOCSWINSZ, &terminal))
387 + {
388 + LOG_ERROR("ioctl({}, TIOCSWINSZ) failed {}", Message.TtyMaster, errno);
389 + }
390 + }
391 + }
392 +
393 + // Shutdown sockets and tty
394 + UtilSocketShutdown(Message.Socket, SHUT_WR);
395 +}
396 +
397 +void HandleMessageImpl(
398 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_FORK& Message, const gsl::span<gsl::byte>& Buffer)
399 +{
400 + sockaddr_vm SocketAddress{};
401 + wil::unique_fd ListenSocket{UtilListenVsockAnyPort(&SocketAddress, 1, true)};
402 + THROW_LAST_ERROR_IF(!ListenSocket);
403 +
404 + WSLC_FORK_RESULT Response{};
405 + Response.Header.MessageSize = sizeof(Response);
406 + Response.Header.MessageType = WSLC_FORK_RESULT::Type;
407 + Response.Port = SocketAddress.svm_port;
408 +
409 + std::promise<pid_t> childPid;
410 +
411 + {
412 + auto childLogic = [ListenSocketFd = ListenSocket.get(), SocketAddress, &Channel, &Message, &childPid]() mutable {
413 + bool futureSet = false;
414 + try
415 + {
416 + wil::unique_fd ListenSocket;
417 +
418 + // Close parent channel
419 + if (Message.ForkType == WSLC_FORK::Process || Message.ForkType == WSLC_FORK::Pty)
420 + {
421 + Channel.Close();
422 + }
423 +
424 + if (Message.ForkType == WSLC_FORK::Thread)
425 + {
426 + // If this is a thread, detach from the process' fd table.
427 + // This prevents other threads from creating child processes that could inherit fds that this thread could create.
428 + // N.B. This needs to happen before childPid is signalled to ensure that ListenSocket() is not closed by the parent before getting duplicated in the child's fd table.
429 + THROW_LAST_ERROR_IF(unshare(CLONE_FILES) < 0);
430 + }
431 +
432 + // ListenSocket should only be assigned after this thread is guaranteed to have its own fd table (either via unshare() or a child process).
433 + ListenSocket.reset(ListenSocketFd);
434 + childPid.set_value(getpid());
435 + futureSet = true;
436 +
437 + wil::unique_fd ProcessSocket{UtilAcceptVsock(ListenSocket.get(), SocketAddress, SESSION_LEADER_ACCEPT_TIMEOUT_MS)};
438 + THROW_LAST_ERROR_IF(!ProcessSocket);
439 +
440 + ListenSocket.reset();
441 +
442 + auto subChannel = wsl::shared::SocketChannel{std::move(ProcessSocket), "ForkedChannel"};
443 + ProcessMessages(subChannel);
444 + }
445 + catch (...)
446 + {
447 + LOG_CAUGHT_EXCEPTION();
448 + if (!futureSet)
449 + {
450 + childPid.set_exception(std::current_exception());
451 + }
452 + }
453 + };
454 +
455 + if (Message.ForkType == WSLC_FORK::Thread)
456 + {
457 + std::thread thread{std::move(childLogic)};
458 + thread.detach();
459 +
460 + Response.Pid = childPid.get_future().get();
461 + }
462 + else if (Message.ForkType == WSLC_FORK::Process)
463 + {
464 + Response.Pid = UtilCreateChildProcess("CreateChildProcess", std::move(childLogic));
465 + }
466 + else if (Message.ForkType == WSLC_FORK::Pty)
467 + {
468 + THROW_LAST_ERROR_IF(prctl(PR_SET_CHILD_SUBREAPER, 1) < 0);
469 +
470 + winsize ttySize{};
471 + ttySize.ws_col = Message.TtyColumns;
472 + ttySize.ws_row = Message.TtyRows;
473 +
474 + wil::unique_fd ttyMaster;
475 + auto result = forkpty(ttyMaster.addressof(), nullptr, nullptr, &ttySize);
476 + THROW_ERRNO_IF(errno, result < 0);
477 +
478 + if (result == 0) // Child
479 + {
480 + sigset_t SignalMask;
481 + sigemptyset(&SignalMask);
482 + THROW_LAST_ERROR_IF(sigprocmask(SIG_SETMASK, &SignalMask, nullptr) < 0);
483 +
484 + try
485 + {
486 + childLogic();
487 + }
488 + CATCH_LOG();
489 + exit(0);
490 + }
491 +
492 + Response.PtyMasterFd = ttyMaster.release();
493 + Response.Pid = result;
494 + }
495 + else
496 + {
497 + LOG_ERROR("Unexpected fork type: {}", Message.Type);
498 + THROW_ERRNO(EINVAL);
499 + }
500 + }
501 +
502 + ListenSocket.reset();
503 + Transaction.Send(Response);
504 +}
505 +
506 +void HandleMessageImpl(
507 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_MOUNT& Message, const gsl::span<gsl::byte>& Buffer)
508 +{
509 + WSLC_MOUNT_RESULT response{};
510 + response.Header.MessageType = WSLC_MOUNT_RESULT::Type;
511 + response.Header.MessageSize = sizeof(response);
512 +
513 + try
514 + {
515 + auto readField = [&](unsigned int index) -> const char* {
516 + if (index > 0)
517 + {
518 + return wsl::shared::string::FromSpan(Buffer, index);
519 + }
520 +
521 + return "";
522 + };
523 +
524 + mountutil::ParsedOptions options;
525 + if (Message.OptionsIndex > 0)
526 + {
527 + options = mountutil::MountParseFlags(wsl::shared::string::FromSpan(Buffer, Message.OptionsIndex));
528 + }
529 +
530 + const char* source = readField(Message.SourceIndex);
531 +
532 + const char* target{};
533 + if (WI_IsFlagSet(Message.Flags, WSLC_MOUNT::KernelModules))
534 + {
535 + assert(!g_state.ModulesMountPoint.has_value());
536 +
537 + // Modules need to be mounted to a specific path that depends on the kernel version.
538 +
539 + utsname UnameBuffer{};
540 + THROW_LAST_ERROR_IF(uname(&UnameBuffer) < 0);
541 +
542 + g_state.ModulesMountPoint = std::format("/lib/modules/{}", UnameBuffer.release);
543 + target = g_state.ModulesMountPoint->c_str();
544 + }
545 + else
546 + {
547 + target = readField(Message.DestinationIndex);
548 + }
549 +
550 + // Chroot without OverlayFs is not supported — the chroot logic depends on the overlay target path.
551 + THROW_ERRNO_IF(EINVAL, WI_IsFlagSet(Message.Flags, WSLC_MOUNT::Chroot) && !WI_IsFlagSet(Message.Flags, WSLC_MOUNT::OverlayFs));
552 +
553 + THROW_LAST_ERROR_IF(
554 + UtilMount(source, target, readField(Message.TypeIndex), options.MountFlags, options.StringOptions.c_str(), c_defaultRetryTimeout) < 0);
555 +
556 + std::optional<std::string> overlayTarget;
557 + if (WI_IsFlagSet(Message.Flags, WSLC_MOUNT::OverlayFs))
558 + {
559 + overlayTarget.emplace(target + std::string("-rw"));
560 + if (std::filesystem::exists(overlayTarget->c_str()))
561 + {
562 + LOG_ERROR("Overlay directory already exists: {}", overlayTarget.value());
563 + THROW_ERRNO(EEXIST);
564 + }
565 +
566 + THROW_LAST_ERROR_IF(UtilMountOverlayFs(overlayTarget->c_str(), target));
567 +
568 + if (WI_IsFlagSet(Message.Flags, WSLC_MOUNT::Chroot))
569 + {
570 + // If this is a chroot, simply mounts the overlay on top of the "-rw" folder.
571 + // We'll chroot into it later, so moving the mountpoint isn't needed.
572 + target = overlayTarget->c_str();
573 +
574 + // Move standard filesystem mounts into the chroot.
575 + // MS_MOVE moves the entire subtree, so /dev/pts comes with /dev and /sys/fs/cgroup comes with /sys.
576 + for (const auto* mountPoint : {"/dev", "/proc", "/sys"})
577 + {
578 + auto chrootTarget = std::format("{}{}", target, mountPoint);
579 + std::filesystem::create_directories(chrootTarget);
580 +
581 + THROW_LAST_ERROR_IF(mount(mountPoint, chrootTarget.c_str(), "none", MS_MOVE, nullptr) < 0);
582 + }
583 +
584 + THROW_LAST_ERROR_IF(MountInit(std::format("{}/init", target).c_str()) < 0); // Required to call /gns later
585 +
586 + // If it exists, mount /etc/resolv.conf
587 + if (std::filesystem::exists("/etc/resolv.conf"))
588 + {
589 + THROW_LAST_ERROR_IF(UtilMountFile("/etc/resolv.conf", std::format("{}/etc/resolv.conf", target).c_str()) < 0);
590 + }
591 +
592 + // If the modules were previously mounted, move them to the chroot.
593 + if (g_state.ModulesMountPoint.has_value())
594 + {
595 + auto chrootTarget = std::format("{}/{}", target, g_state.ModulesMountPoint->native());
596 + std::filesystem::create_directories(chrootTarget);
597 +
598 + THROW_LAST_ERROR_IF(mount(g_state.ModulesMountPoint->c_str(), chrootTarget.c_str(), "none", MS_MOVE, nullptr) < 0);
599 + }
600 + }
601 + else
602 + {
603 + // Move the "-rw" mount to its final target.
604 + THROW_LAST_ERROR_IF(mount(overlayTarget->c_str(), target, "none", MS_MOVE, nullptr) < 0);
605 +
606 + // Clean up the underlying mount point
607 + THROW_LAST_ERROR_IF(umount((overlayTarget.value() + "/rw").c_str()));
608 +
609 + std::error_code error;
610 + std::filesystem::remove_all(overlayTarget.value(), error);
611 + if (error.value() != 0)
612 + {
613 + THROW_ERRNO(error.value());
614 + }
615 + }
616 + }
617 +
618 + if (WI_IsFlagSet(Message.Flags, WSLC_MOUNT::Chroot))
619 + {
620 + THROW_LAST_ERROR_IF(Chroot(target) < 0);
621 +
622 + // Recreate the crash dump symlink inside the new root.
623 + CreateCaptureCrashSymlink();
624 + }
625 +
626 + response.Result = 0;
627 + }
628 + catch (...)
629 + {
630 + LOG_CAUGHT_EXCEPTION();
631 + response.Result = wil::ResultFromCaughtException();
632 + }
633 +
634 + Transaction.Send<WSLC_MOUNT_RESULT>(response);
635 +}
636 +
637 +void HandleMessageImpl(
638 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_EXEC& Message, const gsl::span<gsl::byte>& Buffer)
639 +{
640 + auto Executable = wsl::shared::string::FromSpan(Buffer, Message.ExecutableIndex);
641 + auto ArgumentArray = wsl::shared::string::ArrayFromSpan(Buffer, Message.CommandLineIndex);
642 + auto ArgumentPointers = wsl::shared::string::StringPointersFromArray(ArgumentArray, true);
643 +
644 + auto EnvironmentArray = wsl::shared::string::ArrayFromSpan(Buffer, Message.EnvironmentIndex);
645 + auto EnvironmentPointers = wsl::shared::string::StringPointersFromArray(EnvironmentArray, true);
646 +
647 + execve(Executable, (char* const*)(ArgumentPointers.data()), (char* const*)(EnvironmentPointers.data()));
648 +
649 + // Only reached if exec() fails
650 + Transaction.SendResultMessage<int32_t>(errno);
651 +}
652 +
653 +void HandleMessageImpl(
654 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_PORT_RELAY& Message, const gsl::span<gsl::byte>& Buffer)
655 +{
656 + sockaddr_vm SocketAddress{};
657 + wil::unique_fd ListenSocket{UtilListenVsockAnyPort(&SocketAddress, 10, false)};
658 + THROW_LAST_ERROR_IF(!ListenSocket);
659 +
660 + Transaction.SendResultMessage<uint32_t>(SocketAddress.svm_port);
661 + Channel.Close();
662 + UtilSetThreadName("PortRelay");
663 + RunLocalHostRelay(SocketAddress, ListenSocket.get());
664 +}
665 +
666 +void HandleMessageImpl(
667 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_SIGNAL& Message, const gsl::span<gsl::byte>& Buffer)
668 +{
669 + auto result = kill(Message.Pid, Message.Signal);
670 + Transaction.SendResultMessage(result < 0 ? errno : 0);
671 +}
672 +
673 +void HandleMessageImpl(
674 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_UNMOUNT& Message, const gsl::span<gsl::byte>& Buffer)
675 +{
676 + auto result = umount(Message.Buffer) < 0 ? errno : 0;
677 + if (result == 0)
678 + {
679 + result = rmdir(Message.Buffer) < 0 ? errno : 0;
680 + }
681 +
682 + Transaction.SendResultMessage<int32_t>(result);
683 +}
684 +
685 +void HandleMessageImpl(
686 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_DETACH& Message, const gsl::span<gsl::byte>& Buffer)
687 +{
688 + sync();
689 +
690 + Transaction.SendResultMessage<int32_t>(DetachScsiDisk(Message.Lun));
691 +}
692 +
693 +template <typename TMessage, typename... Args>
694 +void HandleMessage(wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, LX_MESSAGE_TYPE Type, const gsl::span<gsl::byte>& Buffer)
695 +{
696 + if (TMessage::Type == Type)
697 + {
698 + if (Buffer.size() < sizeof(TMessage))
699 + {
700 + LOG_ERROR("Received message {}, but size is too small: {}. Expected {}", Type, Buffer.size(), sizeof(TMessage));
701 + THROW_ERRNO(EINVAL);
702 + }
703 +
704 + const auto Message = gslhelpers::try_get_struct<TMessage>(Buffer);
705 + HandleMessageImpl(Channel, Transaction, *Message, Buffer);
706 +
707 + return;
708 + }
709 + else
710 + {
711 + if constexpr (sizeof...(Args) > 0)
712 + {
713 + HandleMessage<Args...>(Channel, Transaction, Type, Buffer);
714 + }
715 + else
716 + {
717 + LOG_ERROR("Received unknown message type: {}", Type);
718 + THROW_ERRNO(EINVAL);
719 + }
720 + }
721 +}
722 +
723 +void HandleMessageImpl(
724 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_WATCH_PROCESSES& Message, const gsl::span<gsl::byte>& Buffer)
725 +{
726 + // Create a signalfd to watch for SIGCHLD
727 + sigset_t mask{};
728 + sigemptyset(&mask);
729 + sigaddset(&mask, SIGCHLD);
730 + THROW_LAST_ERROR_IF(UtilSaveBlockedSignals(mask) < 0);
731 +
732 + wil::unique_fd signalFd = signalfd(-1, &mask, SFD_CLOEXEC);
733 + THROW_LAST_ERROR_IF(signalFd.get() < 0);
734 +
735 + Transaction.SendResultMessage<uint32_t>(0);
736 +
737 + // Poll for either a received signal or a new message on the channel.
738 + pollfd polls[2]{};
739 + polls[0].fd = signalFd.get();
740 + polls[0].events = POLLIN;
741 + polls[1].fd = Channel.Socket();
742 + polls[1].events = POLLIN;
743 +
744 + while (true)
745 + {
746 + auto result = poll(polls, COUNT_OF(polls), -1);
747 + THROW_LAST_ERROR_IF(result < 0);
748 +
749 + // TODO: Check for poll errors
750 + if (polls[0].revents & POLLIN)
751 + {
752 + signalfd_siginfo sigInfo{};
753 + auto bytes = TEMP_FAILURE_RETRY(read(signalFd.get(), &sigInfo, sizeof(signalfd_siginfo)));
754 +
755 + THROW_LAST_ERROR_IF(bytes < 0);
756 + if (bytes != sizeof(sigInfo))
757 + {
758 + LOG_ERROR("Unexpected read size: {} (expected {})", bytes, sizeof(sigInfo));
759 + THROW_ERRNO(EINVAL);
760 + }
761 +
762 + if (sigInfo.ssi_signo != SIGCHLD)
763 + {
764 + LOG_ERROR("Received unexpected signal from signalfd: {}", sigInfo.ssi_signo);
765 + THROW_LAST_ERROR_IF(EINVAL);
766 + }
767 +
768 + // We received a SIGCHLD. This means that one or more children processes have exited.
769 +
770 + bool exitedProcess = false; // Sanity check
771 +
772 + while (true)
773 + {
774 + int status{};
775 + result = waitpid(-1, &status, WNOHANG);
776 + if (result < 0 && errno != ECHILD)
777 + {
778 + THROW_LAST_ERROR();
779 + }
780 +
781 + if (result <= 0)
782 + {
783 + break;
784 + }
785 +
786 + exitedProcess = true;
787 +
788 + WSLC_PROCESS_EXITED message{};
789 + message.Pid = result;
790 + if (WIFSIGNALED(status))
791 + {
792 + message.Signaled = true;
793 + message.Code = WTERMSIG(status);
794 + }
795 + else if (WIFEXITED(status))
796 + {
797 + message.Code = WEXITSTATUS(status);
798 + }
799 + else
800 + {
801 + LOG_ERROR("Received SIGCHLD for process that was neither signaled nor exited. Pid: {}, Status: {}", result, status);
802 + }
803 +
804 + // Async notification - not a transaction reply
805 + Channel.SendMessage(message);
806 + }
807 +
808 + if (!exitedProcess)
809 + {
810 + LOG_ERROR("Received SIGCHLD but no children have exited");
811 + }
812 + }
813 +
814 + if (polls[1].revents & POLLIN)
815 + {
816 + auto [message, _] = Channel.ReceiveMessageOrClosed<MESSAGE_HEADER>();
817 + if (message == nullptr)
818 + {
819 + break;
820 + }
821 + else
822 + {
823 + LOG_ERROR("Received unexpected message: {}", message->MessageType);
824 + THROW_ERRNO(EINVAL);
825 + }
826 + }
827 + }
828 +}
829 +
830 +void ProcessMessage(wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, LX_MESSAGE_TYPE Type, const gsl::span<gsl::byte>& Buffer)
831 +{
832 + try
833 + {
834 + HandleMessage<WSLC_GET_DISK, WSLC_MOUNT, WSLC_EXEC, WSLC_FORK, WSLC_CONNECT, WSLC_SIGNAL, WSLC_TTY_RELAY, WSLC_PORT_RELAY, WSLC_UNMOUNT, WSLC_DETACH, WSLC_ACCEPT, WSLC_WATCH_PROCESSES, WSLC_UNIX_CONNECT>(
835 + Channel, Transaction, Type, Buffer);
836 + }
837 + catch (...)
838 + {
839 + LOG_CAUGHT_EXCEPTION();
840 +
841 + // TODO: error message
842 + }
843 +}
844 +
845 +void ProcessMessages(wsl::shared::SocketChannel& Channel)
846 +{
847 + while (Channel.Connected())
848 + {
849 + auto transaction = Channel.ReceiveTransaction();
850 + auto [Message, Range] = transaction.ReceiveOrClosed<MESSAGE_HEADER>();
851 + if (Message == nullptr)
852 + {
853 + break;
854 + }
855 +
856 + ProcessMessage(Channel, transaction, Message->MessageType, Range);
857 + }
858 +
859 + LOG_INFO("Process {} exiting", getpid());
860 +}
861 +
862 +int WSLCEntryPoint(int Argc, char* Argv[])
863 +{
864 +
865 + //
866 + // Perform initial mounts.
867 + //
868 +
869 + if (UtilMount(nullptr, "/dev", "devtmpfs", MS_SHARED, nullptr) < 0)
870 + {
871 + return -1;
872 + }
873 +
874 + if (UtilMount(nullptr, "/proc", "proc", MS_SHARED, nullptr) < 0)
875 + {
876 + return -1;
877 + }
878 +
879 + if (UtilMount(nullptr, "/sys", "sysfs", MS_SHARED, nullptr) < 0)
880 + {
881 + return -1;
882 + }
883 +
884 + if (UtilMount(nullptr, "/dev/pts", "devpts", MS_NOATIME | MS_NOSUID | MS_NOEXEC, "gid=5,mode=620") < 0)
885 + {
886 + return -1;
887 + }
888 +
889 + if (UtilMount(nullptr, "/sys/fs/cgroup", "cgroup2", 0, nullptr) < 0)
890 + {
891 + return -1;
892 + }
893 +
894 + //
895 + // Open kmesg for logging and ensure that the file descriptor is not set to one of the standard file descriptors.
896 + //
897 + // N.B. This is to work around a rare race condition where init is launched without /dev/console set as the controlling terminal.
898 + //
899 +
900 + InitializeLogging(false);
901 + if (g_LogFd <= STDERR_FILENO)
902 + {
903 + LOG_ERROR("/init was started without /dev/console");
904 + if (dup2(g_LogFd, 3) < 0)
905 + {
906 + LOG_ERROR("dup2 failed {}", errno);
907 + }
908 +
909 + close(g_LogFd);
910 + g_LogFd = 3;
911 + }
912 +
913 + //
914 + // Increase the soft and hard limit for number of open file descriptors.
915 + // N.B. the soft limit shouldn't be too high. See https://github.com/microsoft/WSL/issues/12985 .
916 + //
917 +
918 + rlimit Limit{};
919 + Limit.rlim_cur = 1024 * 10;
920 + Limit.rlim_max = 1024 * 1024;
921 + if (setrlimit(RLIMIT_NOFILE, &Limit) < 0)
922 + {
923 + LOG_ERROR("setrlimit(RLIMIT_NOFILE) failed {}", errno);
924 + return -1;
925 + }
926 +
927 + Limit.rlim_cur = 0x4000000;
928 + Limit.rlim_max = 0x4000000;
929 + if (setrlimit(RLIMIT_MEMLOCK, &Limit) < 0)
930 + {
931 + LOG_ERROR("setrlimit(RLIMIT_MEMLOCK) failed {}", errno);
932 + return -1;
933 + }
934 +
935 + //
936 + // Enable dump collection when processes crash.
937 + //
938 +
939 + WSLCEnableCrashDumpCollection();
940 +
941 + //
942 + // Enable logging when processes receive fatal signals.
943 + //
944 +
945 + if (WriteToFile("/proc/sys/kernel/print-fatal-signals", "1\n") < 0)
946 + {
947 + return -1;
948 + }
949 +
950 + //
951 + // Disable rate limiting of user writes to dmesg.
952 + //
953 +
954 + if (WriteToFile("/proc/sys/kernel/printk_devkmsg", "on\n") < 0)
955 + {
956 + return -1;
957 + }
958 +
959 + //
960 + // Set the ephemeral port range
961 + //
962 +
963 + if (WriteToFile(
964 + "/proc/sys/net/ipv4/ip_local_port_range",
965 + std::format("{} {}", c_ephemeralPortRange.first, c_ephemeralPortRange.second).c_str()) < 0)
966 + {
967 + return -1;
968 + }
969 +
970 + THROW_LAST_ERROR_IF(UtilSetSignalHandlers(g_SavedSignalActions, false) < 0);
971 +
972 + sigset_t mask{};
973 + sigemptyset(&mask);
974 + sigaddset(&mask, SIGCHLD);
975 + THROW_LAST_ERROR_IF(UtilSaveBlockedSignals(mask) < 0);
976 +
977 + //
978 + // Ensure /dev/console is present and set as the controlling terminal.
979 + // If opening /dev/console times out, stdout and stderr to the logging file descriptor.
980 + //
981 +
982 + wil::unique_fd ConsoleFd{};
983 +
984 + try
985 + {
986 +
987 + wsl::shared::retry::RetryWithTimeout<void>(
988 + [&]() {
989 + ConsoleFd = open("/dev/console", O_RDWR | O_CLOEXEC);
990 + THROW_LAST_ERROR_IF(!ConsoleFd);
991 + },
992 + c_defaultRetryPeriod,
993 + c_defaultRetryTimeout);
994 +
995 + THROW_LAST_ERROR_IF(login_tty(ConsoleFd.get()) < 0);
996 + }
997 + catch (...)
998 + {
999 + if (dup3(g_LogFd, STDOUT_FILENO, O_CLOEXEC) < 0)
1000 + {
1001 + LOG_ERROR("dup2 failed {}", errno);
1002 + }
1003 +
1004 + if (dup3(g_LogFd, STDERR_FILENO, O_CLOEXEC) < 0)
1005 + {
1006 + LOG_ERROR("dup2 failed {}", errno);
1007 + }
1008 + }
1009 +
1010 + //
1011 + // Open /dev/null for stdin.
1012 + //
1013 +
1014 + {
1015 + wil::unique_fd Fd{TEMP_FAILURE_RETRY(open("/dev/null", O_RDONLY))};
1016 + if (!Fd)
1017 + {
1018 + LOG_ERROR("open({}) failed {}", "/dev/null", errno);
1019 + return -1;
1020 + }
1021 +
1022 + if (Fd.get() == STDIN_FILENO)
1023 + {
1024 + Fd.release();
1025 + }
1026 + else
1027 + {
1028 + if (TEMP_FAILURE_RETRY(dup2(Fd.get(), STDIN_FILENO)) < 0)
1029 + {
1030 + LOG_ERROR("dup2 failed {}", errno);
1031 + return -1;
1032 + }
1033 + }
1034 + }
1035 +
1036 + //
1037 + // Enable the loopback interface.
1038 + //
1039 +
1040 + {
1041 + wil::unique_fd Fd{socket(AF_INET, SOCK_DGRAM, IPPROTO_IP)};
1042 + if (!Fd)
1043 + {
1044 + LOG_ERROR("socket failed {}", errno);
1045 + return -1;
1046 + }
1047 +
1048 + if (EnableInterface(Fd.get(), "lo") < 0)
1049 + {
1050 + return -1;
1051 + }
1052 + }
1053 +
1054 + //
1055 + // Make sure not to leak std fds to user processes.
1056 + //
1057 +
1058 + for (int fd : {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO})
1059 + {
1060 + SetCloseOnExec(fd, true);
1061 + }
1062 +
1063 + //
1064 + // Establish the message channel with the service via hvsocket.
1065 + //
1066 +
1067 + wsl::shared::SocketChannel channel = {UtilConnectVsock(LX_INIT_UTILITY_VM_INIT_PORT, true), "mini_init"};
1068 + if (channel.Socket() < 0)
1069 + {
1070 + FATAL_ERROR("Failed to connect to host hvsocket");
1071 + }
1072 + try
1073 + {
1074 + ProcessMessages(channel);
1075 + }
1076 + CATCH_LOG();
1077 +
1078 + LOG_INFO("Init exiting");
1079 +
1080 + try
1081 + {
1082 + auto children = ListInitChildProcesses();
1083 +
1084 + while (!children.empty())
1085 + {
1086 +
1087 + // send SIGKILL to all running processes.
1088 + for (auto pid : children)
1089 + {
1090 + if (kill(pid, SIGKILL) < 0)
1091 + {
1092 + LOG_ERROR("Failed to send SIGKILL to {}: {}", pid, errno);
1093 + }
1094 + }
1095 +
1096 + // Wait for processes to actually exit.
1097 + while (!children.empty())
1098 + {
1099 + auto Result = waitpid(-1, nullptr, 0);
1100 + THROW_ERRNO_IF(errno, Result <= 0);
1101 + LOG_INFO("Process {} exited", Result);
1102 + children.erase(Result);
1103 + }
1104 +
1105 + children = ListInitChildProcesses();
1106 + }
1107 + }
1108 + CATCH_LOG();
1109 +
1110 + sync();
1111 +
1112 + try
1113 + {
1114 + for (auto disk : ListScsiDisks())
1115 + {
1116 + if (DetachScsiDisk(disk) < 0)
1117 + {
1118 + LOG_ERROR("Failed to detach disk: {}", disk);
1119 + }
1120 + }
1121 + }
1122 + CATCH_LOG();
1123 +
1124 + reboot(RB_POWER_OFF);
1125 +
1126 + return 0;
1127 +}
\ No newline at end of file
src/linux/init/common.h
+12 -2
@@ -138,12 +138,22 @@ auto LogImpl(int fd, const std::format_string<Args...>& format, Args&&... args)
138
139 #define GNS_LOG_INFO(str, ...) \
140 { \
141 - LogImpl(g_TelemetryFd, "{}: {} - " str "\n", g_threadName.c_str(), __FUNCTION__, ##__VA_ARGS__); \
141 + if (g_TelemetryFd != -1) \
142 + { \
143 + LogImpl(g_TelemetryFd, "{}: {} - " str "\n", g_threadName.c_str(), __FUNCTION__, ##__VA_ARGS__); \
144 + } \
145 }
146
147 #define GNS_LOG_ERROR(str, ...) \
148 { \
146 - LogImpl(g_TelemetryFd, "{}: {} - ERROR: " str "\n", g_threadName.c_str(), __FUNCTION__, ##__VA_ARGS__); \
149 + if (g_TelemetryFd != -1) \
150 + { \
151 + LogImpl(g_TelemetryFd, "{}: {} - ERROR: " str "\n", g_threadName.c_str(), __FUNCTION__, ##__VA_ARGS__); \
152 + } \
153 + else \
154 + { \
155 + LOG_ERROR(str, ##__VA_ARGS__); \
156 + } \
157 }
158
159 #define FATAL_ERROR(str, ...) FATAL_ERROR_EX(1, str, ##__VA_ARGS__)
src/linux/init/localhost.cpp
+121 -120
@@ -30,125 +30,6 @@
30
31 namespace {
32
33 -void ListenThread(sockaddr_vm hvSocketAddress, int listenSocket)
34 -{
35 - pollfd pollDescriptors[] = {{listenSocket, POLLIN}};
36 - for (;;)
37 - {
38 - int result = poll(pollDescriptors, COUNT_OF(pollDescriptors), -1);
39 - if (result < 0)
40 - {
41 - LOG_ERROR("poll failed {}", errno);
42 - return;
43 - }
44 -
45 - if ((pollDescriptors[0].revents & POLLIN) == 0)
46 - {
47 - LOG_ERROR("unexpected revents {:x}", pollDescriptors[0].revents);
48 - return;
49 - }
50 -
51 - // Accept a connection and start a relay worker thread.
52 - wil::unique_fd relaySocket{UtilAcceptVsock(listenSocket, hvSocketAddress)};
53 - THROW_LAST_ERROR_IF(!relaySocket);
54 -
55 - std::thread([relaySocket = std::move(relaySocket)]() {
56 - try
57 - {
58 - // Read a message to determine which TCP port to connect to.
59 - std::vector<gsl::byte> buffer(sizeof(LX_INIT_START_SOCKET_RELAY));
60 - auto bytesRead = UtilReadBuffer(relaySocket.get(), buffer);
61 - if (bytesRead == 0)
62 - {
63 - return;
64 - }
65 -
66 - auto* message = gslhelpers::try_get_struct<LX_INIT_START_SOCKET_RELAY>(gsl::make_span(buffer.data(), bytesRead));
67 - THROW_ERRNO_IF(EINVAL, !message || (message->Header.MessageType != LxInitMessageStartSocketRelay));
68 -
69 - // Connect to the actual socket address and set up a relay.
70 - //
71 - // N.B. While the relay was being set up, the server may have
72 - // stopped listening.
73 - sockaddr* socketAddress;
74 - int socketAddressSize;
75 - sockaddr_in sockaddrIn{};
76 - sockaddr_in6 sockaddrIn6{};
77 - if (message->Family == AF_INET)
78 - {
79 - sockaddrIn.sin_family = AF_INET;
80 - sockaddrIn.sin_port = htons(message->Port);
81 - sockaddrIn.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
82 - socketAddress = reinterpret_cast<sockaddr*>(&sockaddrIn);
83 - socketAddressSize = sizeof(sockaddrIn);
84 - }
85 - else if (message->Family == AF_INET6)
86 - {
87 - sockaddrIn6.sin6_family = AF_INET6;
88 - sockaddrIn6.sin6_port = htons(message->Port);
89 - sockaddrIn6.sin6_addr = IN6ADDR_LOOPBACK_INIT;
90 - socketAddress = reinterpret_cast<sockaddr*>(&sockaddrIn6);
91 - socketAddressSize = sizeof(sockaddrIn6);
92 - }
93 - else
94 - {
95 - THROW_ERRNO(EINVAL);
96 - }
97 -
98 - wil::unique_fd tcpSocket{socket(socketAddress->sa_family, SOCK_STREAM, IPPROTO_TCP)};
99 - THROW_LAST_ERROR_IF(!tcpSocket);
100 -
101 - if (TEMP_FAILURE_RETRY(connect(tcpSocket.get(), socketAddress, socketAddressSize)) < 0)
102 - {
103 - return;
104 - }
105 -
106 - // Resize the buffer to be the requested size.
107 - buffer.resize(message->BufferSize);
108 -
109 - // Begin relaying data.
110 - int outFd[2] = {tcpSocket.get(), relaySocket.get()};
111 - pollfd pollDescriptors[] = {{relaySocket.get(), POLLIN}, {tcpSocket.get(), POLLIN}};
112 -
113 - for (;;)
114 - {
115 - if ((pollDescriptors[0].fd == -1) || (pollDescriptors[1].fd == -1))
116 - {
117 - return;
118 - }
119 -
120 - THROW_LAST_ERROR_IF(poll(pollDescriptors, COUNT_OF(pollDescriptors), -1) < 0);
121 -
122 - bytesRead = 0;
123 - for (int Index = 0; Index < COUNT_OF(pollDescriptors); Index += 1)
124 - {
125 - if (pollDescriptors[Index].revents & POLLIN)
126 - {
127 - bytesRead = UtilReadBuffer(pollDescriptors[Index].fd, buffer);
128 - if (bytesRead == 0)
129 - {
130 - pollDescriptors[Index].fd = -1;
131 - shutdown(outFd[Index], SHUT_WR);
132 - }
133 - else if (bytesRead < 0)
134 - {
135 - return;
136 - }
137 - else if (UtilWriteBuffer(outFd[Index], buffer.data(), bytesRead) < 0)
138 - {
139 - return;
140 - }
141 - }
142 - }
143 - }
144 - }
145 - CATCH_LOG()
146 - }).detach();
147 - }
148 -
149 - return;
150 -}
151 -
33 std::vector<sockaddr_storage> QueryListeningSockets(NetlinkChannel& channel)
34 {
35 std::vector<sockaddr_storage> sockets{};
@@ -349,6 +230,126 @@ int MonitorListeningSockets(wsl::shared::SocketChannel& channel)
230 }
231 } // namespace
232
233 +void RunLocalHostRelay(sockaddr_vm hvSocketAddress, int listenSocket)
234 +{
235 + pollfd pollDescriptors[] = {{listenSocket, POLLIN}};
236 + for (;;)
237 + {
238 + int result = poll(pollDescriptors, COUNT_OF(pollDescriptors), -1);
239 + if (result < 0)
240 + {
241 + LOG_ERROR("poll failed {}", errno);
242 + return;
243 + }
244 +
245 + if ((pollDescriptors[0].revents & POLLIN) == 0)
246 + {
247 + LOG_ERROR("unexpected revents {:x}", pollDescriptors[0].revents);
248 + return;
249 + }
250 +
251 + // Accept a connection and start a relay worker thread.
252 + wil::unique_fd relaySocket{UtilAcceptVsock(listenSocket, hvSocketAddress)};
253 + THROW_LAST_ERROR_IF(!relaySocket);
254 +
255 + std::thread([relaySocket = std::move(relaySocket)]() {
256 + try
257 + {
258 + // Read a message to determine which TCP port to connect to.
259 + std::vector<gsl::byte> buffer(sizeof(LX_INIT_START_SOCKET_RELAY));
260 + auto bytesRead = UtilReadBuffer(relaySocket.get(), buffer);
261 + if (bytesRead == 0)
262 + {
263 + return;
264 + }
265 +
266 + auto* message = gslhelpers::try_get_struct<LX_INIT_START_SOCKET_RELAY>(gsl::make_span(buffer.data(), bytesRead));
267 + THROW_ERRNO_IF(EINVAL, !message || (message->Header.MessageType != LxInitMessageStartSocketRelay));
268 +
269 + // Connect to the actual socket address and set up a relay.
270 + //
271 + // N.B. During the time setting up the relay the server may have
272 + // stopped listening.
273 + sockaddr* socketAddress;
274 + int socketAddressSize;
275 + sockaddr_in sockaddrIn{};
276 + sockaddr_in6 sockaddrIn6{};
277 +
278 + if (message->Family == AF_INET)
279 + {
280 + sockaddrIn.sin_family = AF_INET;
281 + sockaddrIn.sin_port = htons(message->Port);
282 + sockaddrIn.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
283 + socketAddress = reinterpret_cast<sockaddr*>(&sockaddrIn);
284 + socketAddressSize = sizeof(sockaddrIn);
285 + }
286 + else if (message->Family == AF_INET6)
287 + {
288 + sockaddrIn6.sin6_family = AF_INET6;
289 + sockaddrIn6.sin6_port = htons(message->Port);
290 + sockaddrIn6.sin6_addr = IN6ADDR_LOOPBACK_INIT;
291 + socketAddress = reinterpret_cast<sockaddr*>(&sockaddrIn6);
292 + socketAddressSize = sizeof(sockaddrIn6);
293 + }
294 + else
295 + {
296 + THROW_ERRNO(EINVAL);
297 + }
298 +
299 + wil::unique_fd tcpSocket{socket(socketAddress->sa_family, SOCK_STREAM, IPPROTO_TCP)};
300 + THROW_LAST_ERROR_IF(!tcpSocket);
301 +
302 + if (TEMP_FAILURE_RETRY(connect(tcpSocket.get(), socketAddress, socketAddressSize)) < 0)
303 + {
304 + return;
305 + }
306 +
307 + // Resize the buffer to be the requested size.
308 + buffer.resize(message->BufferSize);
309 +
310 + // Begin relaying data.
311 + int outFd[2] = {tcpSocket.get(), relaySocket.get()};
312 + pollfd pollDescriptors[] = {{relaySocket.get(), POLLIN}, {tcpSocket.get(), POLLIN}};
313 +
314 + for (;;)
315 + {
316 + if ((pollDescriptors[0].fd == -1) || (pollDescriptors[1].fd == -1))
317 + {
318 + return;
319 + }
320 +
321 + THROW_LAST_ERROR_IF(poll(pollDescriptors, COUNT_OF(pollDescriptors), -1) < 0);
322 +
323 + bytesRead = 0;
324 + for (int Index = 0; Index < COUNT_OF(pollDescriptors); Index += 1)
325 + {
326 + if (pollDescriptors[Index].revents & POLLIN)
327 + {
328 + bytesRead = UtilReadBuffer(pollDescriptors[Index].fd, buffer);
329 + if (bytesRead == 0)
330 + {
331 + pollDescriptors[Index].fd = -1;
332 + shutdown(outFd[Index], SHUT_WR);
333 + }
334 + else if (bytesRead < 0)
335 + {
336 + return;
337 + }
338 + else if (UtilWriteBuffer(outFd[Index], buffer.data(), bytesRead) < 0)
339 + {
340 + return;
341 + }
342 + }
343 + }
344 + }
345 + }
346 + CATCH_LOG()
347 + }).detach();
348 + }
349 +
350 + return;
351 +}
352 +
353 // Create a thread to monitor for connections to relay.
354 int StartLocalhostRelay(wsl::shared::SocketChannel& channel, int GuestRelayFd, bool ScanForPorts)
355 try
@@ -373,7 +374,7 @@ try
374 std::thread([hvSocketAddress, listenSocket = std::move(listenSocket)]() {
375 try
376 {
376 - ListenThread(hvSocketAddress, listenSocket.get());
377 + RunLocalHostRelay(hvSocketAddress, listenSocket.get());
378 }
379 CATCH_LOG()
380 }).detach();
src/linux/init/localhost.h
+2
@@ -2,3 +2,5 @@
2 #pragma once
3
4 int RunPortTracker(int argc, char** argv);
5 +
6 +void RunLocalHostRelay(sockaddr_vm hvSocketAddress, int listenSocket);
\ No newline at end of file
src/linux/init/main.cpp
+16 -11
@@ -1528,17 +1528,11 @@ Return Value:
1528 // Create a tmpfs mount for the cross-distro shared mount.
1529 //
1530
1531 - if (UtilMount(nullptr, CROSS_DISTRO_SHARE_PATH, "tmpfs", 0, nullptr) < 0)
1531 + if (UtilMount(nullptr, CROSS_DISTRO_SHARE_PATH, "tmpfs", MS_SHARED, nullptr) < 0)
1532 {
1533 return -1;
1534 }
1535
1536 - if (mount(nullptr, CROSS_DISTRO_SHARE_PATH, nullptr, MS_SHARED, nullptr) < 0)
1537 - {
1538 - LOG_ERROR("mount({}, MS_SHARED) failed {}", CROSS_DISTRO_SHARE_PATH, errno);
1539 - return -1;
1540 - }
1541 -
1536 //
1537 // Create the resolv.conf symlink in the cross-distro share (gns writes to /etc/resolv.conf).
1538 //
@@ -1624,11 +1618,12 @@ Return Value:
1618 }
1619
1620 // Initialize logging to the hvc console device responsible for logging telemetry.
1621 + // If the device is not present, error messages will be logged to kmesg.
1622 if (UtilIsUtilityVm())
1623 {
1624 devicePath = DEVFS_PATH "/" LX_INIT_HVC_TELEMETRY;
1625 g_TelemetryFd = TEMP_FAILURE_RETRY(open(devicePath, (O_WRONLY | O_CLOEXEC)));
1631 - if (g_TelemetryFd < 0)
1626 + if (g_TelemetryFd < 0 && errno != ENODEV)
1627 {
1628 LOG_ERROR("open({}) failed {}", devicePath, errno);
1629 }
@@ -2523,9 +2518,7 @@ void ProcessLaunchInitMessage(
2518 // Create a tmpfs mount for a shared folder between user and system distro.
2519 //
2520
2526 - THROW_LAST_ERROR_IF(UtilMount(nullptr, WSLG_PATH, "tmpfs", 0, nullptr) < 0);
2527 -
2528 - THROW_LAST_ERROR_IF(mount(nullptr, WSLG_PATH, nullptr, MS_SHARED, nullptr) < 0);
2521 + THROW_LAST_ERROR_IF(UtilMount(nullptr, WSLG_PATH, "tmpfs", MS_SHARED, nullptr) < 0);
2522
2523 //
2524 // Create a directory to store x11 sockets.
@@ -3866,6 +3859,8 @@ Return Value:
3859
3860 int WslEntryPoint(int Argc, char* Argv[]);
3861
3862 +extern int WSLCEntryPoint(int Argc, char* Argv[]);
3863 +
3864 void EnableDebugMode(const std::string& Mode)
3865 {
3866 if (Mode == "hvsocket")
@@ -3941,6 +3936,16 @@ int main(int Argc, char* Argv[])
3936 // Determine which entrypoint should be used.
3937 //
3938
3939 + if (getenv(WSLC_ROOT_INIT_ENV))
3940 + {
3941 + if (unsetenv(WSLC_ROOT_INIT_ENV))
3942 + {
3943 + LOG_ERROR("unsetenv failed {}", errno);
3944 + }
3945 +
3946 + return WSLCEntryPoint(Argc, Argv);
3947 + }
3948 +
3949 if (getpid() != 1 || !getenv(WSL_ROOT_INIT_ENV))
3950 {
3951 return WslEntryPoint(Argc, Argv);
src/linux/init/util.cpp
+40 -4
@@ -199,7 +199,7 @@ InteropServer::~InteropServer()
199 Reset();
200 }
201
202 -int UtilAcceptVsock(int SocketFd, sockaddr_vm SocketAddress, int Timeout)
202 +int UtilAcceptVsock(int SocketFd, sockaddr_vm SocketAddress, int Timeout, int SocketFlags)
203
204 /*++
205
@@ -217,6 +217,8 @@ Arguments:
217
218 Timeout - Supplies a timeout.
219
220 + SocketFlags - Supplies the socket flags.
221 +
222 Return Value:
223
224 A file descriptor representing the socket, -1 on failure.
@@ -265,7 +267,7 @@ Return Value:
267 if (Result != -1)
268 {
269 socklen_t SocketAddressSize = sizeof(SocketAddress);
268 - Result = accept4(SocketFd, reinterpret_cast<sockaddr*>(&SocketAddress), &SocketAddressSize, SOCK_CLOEXEC);
270 + Result = accept4(SocketFd, reinterpret_cast<sockaddr*>(&SocketAddress), &SocketAddressSize, SocketFlags);
271 }
272
273 if (Result < 0)
@@ -1706,6 +1708,25 @@ Return Value:
1708 return 0;
1709 }
1710
1711 +int UtilMountFile(const char* Source, const char* Destination)
1712 +try
1713 +{
1714 + // Is the file is a symlink, delete it since that would break the mount.
1715 + if (std::filesystem::is_symlink(Destination))
1716 + {
1717 + std::filesystem::remove(Destination);
1718 + }
1719 +
1720 + wil::unique_fd Fd{open(Destination, (O_CREAT | O_WRONLY), 0755)};
1721 + THROW_LAST_ERROR_IF(!Fd);
1722 +
1723 + THROW_LAST_ERROR_IF(mount(Source, Destination, nullptr, (MS_RDONLY | MS_BIND), nullptr) < 0);
1724 + THROW_LAST_ERROR_IF(mount(nullptr, Destination, nullptr, (MS_RDONLY | MS_REMOUNT | MS_BIND), nullptr) < 0);
1725 +
1726 + return 0;
1727 +}
1728 +CATCH_RETURN_ERRNO();
1729 +
1730 int UtilMount(const char* Source, const char* Target, const char* Type, unsigned long MountFlags, const char* Options, std::optional<std::chrono::seconds> TimeoutSeconds)
1731
1732 /*++
@@ -1752,13 +1773,18 @@ Return Value:
1773 // - For Plan9 (9p): device is busy or not found
1774 // - For VirtioFS: invalid tag (device not ready)
1775 //
1776 + // N.B. MS_SHARED must be applied in a separate mount() call, so it is
1777 + // stripped from the initial mount flags and applied after the mount.
1778 + //
1779 +
1780 + const unsigned long initialFlags = MountFlags & ~MS_SHARED;
1781
1782 try
1783 {
1784 if (TimeoutSeconds.has_value())
1785 {
1786 wsl::shared::retry::RetryWithTimeout<void>(
1761 - [&]() { THROW_LAST_ERROR_IF(mount(Source, Target, Type, MountFlags, Options) < 0); },
1787 + [&]() { THROW_LAST_ERROR_IF(mount(Source, Target, Type, initialFlags, Options) < 0); },
1788 c_defaultRetryPeriod,
1789 TimeoutSeconds.value(),
1790 [&]() {
@@ -1784,7 +1810,7 @@ Return Value:
1810 }
1811 else
1812 {
1787 - THROW_LAST_ERROR_IF(mount(Source, Target, Type, MountFlags, Options) < 0);
1813 + THROW_LAST_ERROR_IF(mount(Source, Target, Type, initialFlags, Options) < 0);
1814 }
1815 }
1816 catch (...)
@@ -1794,6 +1820,16 @@ Return Value:
1820 return -errno;
1821 }
1822
1823 + // N.B. The shared flag must be applied in a separate mount() call.
1824 + if (WI_IsFlagSet(MountFlags, MS_SHARED))
1825 + {
1826 + if (mount(nullptr, Target, nullptr, MS_SHARED, nullptr) < 0)
1827 + {
1828 + LOG_ERROR("Failed to make shared mount {} {}", Target, errno);
1829 + return -errno;
1830 + }
1831 + }
1832 +
1833 return 0;
1834 }
1835
src/linux/init/util.h
+3 -1
@@ -118,7 +118,7 @@ private:
118 wil::unique_fd m_InteropSocket;
119 };
120
121 -int UtilAcceptVsock(int SocketFd, sockaddr_vm Address, int Timeout = -1);
121 +int UtilAcceptVsock(int SocketFd, sockaddr_vm Address, int Timeout = -1, int SocketFlags = SOCK_CLOEXEC);
122
123 int UtilBindVsockAnyPort(struct sockaddr_vm* SocketAddress, int Type);
124
@@ -251,6 +251,8 @@ int UtilMkdir(const char* Path, mode_t Mode);
251
252 int UtilMkdirPath(const char* Path, mode_t Mode, bool SkipLast = false);
253
254 +int UtilMountFile(const char* Source, const char* Destination);
255 +
256 int UtilMount(const char* Source, const char* Target, const char* Type, unsigned long MountFlags, const char* Options, std::optional<std::chrono::seconds> TimeoutSeconds = {});
257
258 int UtilMountOverlayFs(const char* Target, const char* Lower, unsigned long MountFlags = 0, std::optional<std::chrono::seconds> TimeoutSeconds = {});
src/linux/init/wslpath.cpp
+1 -1
@@ -444,7 +444,7 @@ Return Value:
444 constexpr auto Usage = std::bind(Localization::MessageWslPathUsage, Localization::Options::Default);
445
446 parser.AddPositionalArgument(OriginalPath, 0);
447 - parser.AddArgument(SetFlag<int, TRANSLATE_FLAG_ABSOLUTE>{Flags}, nullptr, TRANSLATE_MODE_ABSOLUTE);
447 + parser.AddArgument(SetFlag<TRANSLATE_FLAG_ABSOLUTE, int>{Flags}, nullptr, TRANSLATE_MODE_ABSOLUTE);
448 parser.AddArgument(UniqueSetValue<char, TRANSLATE_MODE_UNIX>{Mode, Usage}, nullptr, TRANSLATE_MODE_UNIX);
449 parser.AddArgument(UniqueSetValue<char, TRANSLATE_MODE_WINDOWS>{Mode, Usage}, nullptr, TRANSLATE_MODE_WINDOWS);
450 parser.AddArgument(UniqueSetValue<char, TRANSLATE_MODE_MIXED>{Mode, Usage}, nullptr, TRANSLATE_MODE_MIXED);
src/shared/inc/CommandLine.h
+83 -19
@@ -44,7 +44,7 @@ struct Argument
44 bool Positional;
45 };
46
47 -template <typename T, T Flag>
47 +template <auto Flag, typename T = std::remove_reference_t<decltype(Flag)>>
48 struct SetFlag
49 {
50 T& value;
@@ -55,6 +55,17 @@ struct SetFlag
55 }
56 };
57
58 +template <auto Flag, typename T = std::remove_reference_t<decltype(Flag)>>
59 +struct ClearFlag
60 +{
61 + T& value;
62 +
63 + void operator()() const
64 + {
65 + WI_ClearFlag(value, Flag);
66 + }
67 +};
68 +
69 template <typename T>
70 struct is_optional : std::false_type
71 {
@@ -162,9 +173,10 @@ struct AbsolutePath
173 }
174 };
175
176 +template <typename THandle = wil::unique_handle>
177 struct Handle
178 {
167 - wil::unique_handle& output;
179 + THandle& output;
180
181 int operator()(const TChar* input) const
182 {
@@ -173,7 +185,31 @@ struct Handle
185 return -1;
186 }
187
176 - output.reset(ULongToHandle(wcstoul(input, nullptr, 0)));
188 + if constexpr (std::is_same_v<THandle, wil::unique_socket>)
189 + {
190 + output.reset(reinterpret_cast<SOCKET>(ULongToHandle(wcstoul(input, nullptr, 0))));
191 + }
192 + else
193 + {
194 + output.reset(ULongToHandle(wcstoul(input, nullptr, 0)));
195 + }
196 +
197 + return 1;
198 + }
199 +};
200 +
201 +struct Utf8String
202 +{
203 + std::string& Value;
204 +
205 + int operator()(const TChar* Input) const
206 + {
207 + if (Input == nullptr)
208 + {
209 + return -1;
210 + }
211 +
212 + Value = wsl::shared::string::WideToMultiByte(Input);
213
214 return 1;
215 }
@@ -282,7 +318,8 @@ class ArgumentParser
318 public:
319 #ifdef WIN32
320
285 - ArgumentParser(const std::wstring& CommandLine, LPCWSTR Name, int StartIndex = 1) : m_startIndex(StartIndex), m_name(Name)
321 + ArgumentParser(const std::wstring& CommandLine, LPCWSTR Name, int StartIndex = 1, bool ignoreUnknownArgs = false) :
322 + m_parseIndex(StartIndex), m_name(Name), m_ignoreUnknownArgs(ignoreUnknownArgs)
323 {
324 m_argv.reset(CommandLineToArgvW(std::wstring(CommandLine).c_str(), &m_argc));
325 THROW_LAST_ERROR_IF(!m_argv);
@@ -290,7 +327,8 @@ public:
327
328 #else
329
293 - ArgumentParser(int argc, const char* const* argv) : m_argc(argc), m_argv(argv), m_startIndex(1)
330 + ArgumentParser(int argc, const char* const* argv, bool ignoreUnknownArgs = false) :
331 + m_argc(argc), m_argv(argv), m_parseIndex(1), m_ignoreUnknownArgs(ignoreUnknownArgs)
332 {
333 }
334
@@ -327,13 +365,13 @@ public:
365 m_arguments.emplace_back(std::move(match), BuildParseMethod(std::forward<T>(Output)), true);
366 }
367
330 - void Parse() const
368 + void Parse()
369 {
370 int argumentPosition = 0;
371 bool stopParameters = false;
334 - for (size_t i = m_startIndex; i < m_argc; i++)
372 + for (; m_parseIndex < m_argc; m_parseIndex++)
373 {
336 - if (!stopParameters && wsl::shared::string::IsEqual(m_argv[i], TEXT("--")))
374 + if (!stopParameters && wsl::shared::string::IsEqual(m_argv[m_parseIndex], TEXT("--")))
375 {
376 stopParameters = true;
377 continue;
@@ -343,9 +381,10 @@ public:
381 int offset = 0;
382
383 // Special case for short argument with multiple values like -abc
346 - if (!stopParameters && m_argv[i][0] == '-' && m_argv[i][1] != '-' && m_argv[i][1] != '\0' && m_argv[i][2] != '\0')
384 + if (!stopParameters && m_argv[m_parseIndex][0] == '-' && m_argv[m_parseIndex][1] != '-' &&
385 + m_argv[m_parseIndex][1] != '\0' && m_argv[m_parseIndex][2] != '\0')
386 {
348 - for (const auto* arg = &m_argv[i][1]; *arg != '\0'; arg++)
387 + for (const auto* arg = &m_argv[m_parseIndex][1]; *arg != '\0'; arg++)
388 {
389 foundMatch = false;
390 for (const auto& e : m_arguments)
@@ -370,23 +409,26 @@ public:
409 {
410 for (const auto& e : m_arguments)
411 {
373 - if (e.Matches(stopParameters ? nullptr : m_argv[i], m_argv[i][0] == '-' && m_argv[i][1] != '\0' && !stopParameters ? -1 : argumentPosition))
412 + if (e.Matches(
413 + stopParameters ? nullptr : m_argv[m_parseIndex],
414 + m_argv[m_parseIndex][0] == '-' && m_argv[m_parseIndex][1] != '\0' && !stopParameters ? -1 : argumentPosition))
415 {
416 const TChar* value = nullptr;
417 if (e.Positional)
418 {
378 - value = m_argv[i]; // Positional arguments directly receive arvg[i]
419 + value = m_argv[m_parseIndex]; // Positional arguments directly receive argv[i]
420 }
380 - else if (i + 1 < m_argc)
421 + else if (m_parseIndex + 1 < m_argc)
422 {
382 - value = m_argv[i + 1];
423 + value = m_argv[m_parseIndex + 1];
424 }
425
426 offset = e.Consume(value);
427 if (offset < 0)
428 {
429 WI_ASSERT(value == nullptr);
389 - THROW_USER_ERROR(wsl::shared::Localization::MessageMissingArgument(m_argv[i], m_name ? m_name : m_argv[0]));
430 + THROW_USER_ERROR(
431 + wsl::shared::Localization::MessageMissingArgument(m_argv[m_parseIndex], m_name ? m_name : m_argv[0]));
432 }
433
434 if (e.Positional) // Positional arguments can't consume extra arguments.
@@ -394,7 +436,7 @@ public:
436 offset = 0;
437 }
438
397 - i += offset;
439 + m_parseIndex += offset;
440 foundMatch = true;
441
442 break;
@@ -404,16 +446,37 @@ public:
446
447 if (!foundMatch)
448 {
407 - THROW_USER_ERROR(wsl::shared::Localization::MessageInvalidCommandLine(m_argv[i], m_name ? m_name : m_argv[0]));
449 + if (m_ignoreUnknownArgs)
450 + {
451 + break;
452 + }
453 +
454 + THROW_USER_ERROR(wsl::shared::Localization::MessageInvalidCommandLine(m_argv[m_parseIndex], m_name ? m_name : m_argv[0]));
455 }
456
410 - if (i < m_argc && m_argv[i - offset][0] != '-')
457 + if (m_parseIndex < m_argc && m_argv[m_parseIndex - offset][0] != '-')
458 {
459 argumentPosition++;
460 }
461 }
462 }
463
464 + size_t ParseIndex() const noexcept
465 + {
466 + return m_parseIndex;
467 + }
468 +
469 + size_t Argc() const noexcept
470 + {
471 + return m_argc;
472 + }
473 +
474 + const auto* Argv(size_t Index) const noexcept
475 + {
476 + WI_ASSERT(Index < static_cast<size_t>(m_argc));
477 + return m_argv[Index];
478 + }
479 +
480 private:
481 template <typename T>
482 static std::function<int(const TChar*)> BuildParseMethod(T&& Output)
@@ -508,8 +571,9 @@ private:
571
572 #endif
573
511 - int m_startIndex{};
574 + int m_parseIndex{};
575 const TChar* m_name{};
576 + bool m_ignoreUnknownArgs{false};
577 };
578 } // namespace wsl::shared
579
src/shared/inc/JsonUtils.h
+29 -5
@@ -20,6 +20,7 @@ Abstract:
20 #ifdef WIN32
21 #include "wslservice.h"
22 #include "ExecutionContext.h"
23 +#include "wslc.h"
24 #endif
25
26 #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_FROM_ONLY(Type, ...) \
@@ -31,19 +32,21 @@ Abstract:
32
33 namespace wsl::shared {
34
35 +constexpr int c_jsonPrettyPrintIndent = 2;
36 +
37 template <typename T>
35 -std::string ToJson(const T& Value)
38 +std::string ToJson(const T& Value, int indent = -1)
39 {
40 nlohmann::json json;
41 to_json(json, Value);
42
40 - return json.dump();
43 + return json.dump(indent);
44 }
45
46 template <typename T>
44 -std::wstring ToJsonW(const T& Value)
47 +std::wstring ToJsonW(const T& Value, int indent = -1)
48 {
46 - return wsl::shared::string::MultiByteToWide(ToJson(Value));
49 + return wsl::shared::string::MultiByteToWide(ToJson(Value, indent));
50 }
51
52 template <typename T, typename TJson = nlohmann::json>
@@ -62,7 +65,8 @@ T FromJson(const char* Value)
65
66 #ifdef WIN32
67
65 - THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_JSON, wsl::shared::Localization::MessageInvalidJson(e.what()));
68 + THROW_HR_WITH_USER_ERROR_MSG(
69 + WSL_E_INVALID_JSON, wsl::shared::Localization::MessageInvalidJson(e.what()), "Invalid JSON: %hs", Value);
70
71 #else
72 LOG_ERROR("Failed to deserialize json: '{}'. Error: {}", Value, e.what());
@@ -168,4 +172,24 @@ struct adl_serializer<wsl::shared::string::MacAddress>
172 }
173 };
174
175 +#ifdef WIN32
176 +template <>
177 +struct adl_serializer<WSLCVolumeInformation>
178 +{
179 + static void to_json(json& j, const WSLCVolumeInformation& volume)
180 + {
181 + j = json{{"Name", std::string(volume.Name)}, {"Driver", std::string(volume.Driver)}};
182 + }
183 +
184 + static void from_json(const json& j, WSLCVolumeInformation& volume)
185 + {
186 + std::string name = j.at("Name").get<std::string>();
187 + std::string driver = j.at("Driver").get<std::string>();
188 +
189 + strncpy_s(volume.Name, sizeof(volume.Name), name.c_str(), _TRUNCATE);
190 + strncpy_s(volume.Driver, sizeof(volume.Driver), driver.c_str(), _TRUNCATE);
191 + }
192 +};
193 +#endif
194 +
195 } // namespace nlohmann
\ No newline at end of file
src/shared/inc/SocketChannel.h
+70 -24
@@ -109,15 +109,14 @@ public:
109 return *this;
110 }
111
112 - // Note: 'name' must be a global string, since SocketChannel doesn't make a copy of it.
113 - SocketChannel(TSocket&& socket, const char* name) : m_socket(std::move(socket)), m_name(name)
112 + SocketChannel(TSocket&& socket, std::string&& name) : m_socket(std::move(socket)), m_name(std::move(name))
113 {
114 }
115
116 #ifdef WIN32
117
119 - SocketChannel(TSocket&& socket, const char* name, HANDLE exitEvent) :
120 - m_socket(std::move(socket)), m_exitEvent(exitEvent), m_name(name)
118 + SocketChannel(TSocket&& socket, std::string&& name, HANDLE exitEvent) :
119 + m_socket(std::move(socket)), m_exitEvent(exitEvent), m_name(std::move(name))
120 {
121 }
122
@@ -133,7 +132,7 @@ public:
132
133 #ifdef WIN32
134
136 - THROW_HR_MSG(E_UNEXPECTED, "Incorrect channel usage detected on channel: %hs, message type: %hs", m_name, ToString(TMessage::Type));
135 + THROW_HR_MSG(E_UNEXPECTED, "Incorrect channel usage detected on channel: %hs, message type: %hs", m_name.c_str(), ToString(TMessage::Type));
136
137 #else
138
@@ -143,7 +142,7 @@ public:
142 #endif
143 }
144
146 - THROW_INVALID_ARG_IF(m_name == nullptr || span.size() < sizeof(TMessage));
145 + THROW_INVALID_ARG_IF(m_name.empty() || span.size() < sizeof(TMessage));
146
147 auto* header = gslhelpers::try_get_struct<MESSAGE_HEADER>(span);
148 WI_ASSERT(header->MessageSize == span.size());
@@ -166,7 +165,7 @@ public:
165
166 WSL_LOG(
167 "SentMessage",
169 - TraceLoggingValue(m_name, "Name"),
168 + TraceLoggingValue(m_name.c_str(), "Name"),
169 TraceLoggingValue(reinterpret_cast<const TMessage*>(span.data())->PrettyPrint().c_str(), "Content"),
170 TraceLoggingValue(sentBytes, "SentBytes"));
171
@@ -199,6 +198,13 @@ public:
198 }
199 }
200
201 + template <typename TMessage>
202 + void SendMessage()
203 + {
204 + TMessage message;
205 + SendMessage(message);
206 + }
207 +
208 template <typename TMessage>
209 void SendMessage(TMessage& message, uint32_t transactionStep = static_cast<uint32_t>(TRANSACTION_STEP::NONE), uint32_t transactionId = 0)
210 {
@@ -207,7 +213,7 @@ public:
213 if (header.MessageSize != sizeof(message))
214 {
215 #ifdef WIN32
210 - THROW_HR_MSG(E_INVALIDARG, "Incorrect header size for message type: %u on channel: %hs", header.MessageType, m_name);
216 + THROW_HR_MSG(E_INVALIDARG, "Incorrect header size for message type: %u on channel: %hs", header.MessageType, m_name.c_str());
217 #else
218 LOG_ERROR("Incorrect header size for message type: {} on channel: {}", header.MessageType, m_name);
219 THROW_ERRNO(EINVAL);
@@ -234,7 +240,7 @@ public:
240 uint32_t expectedTransactionStep = static_cast<uint32_t>(TRANSACTION_STEP::NONE),
241 uint32_t expectedTransactionId = 0)
242 {
237 - WI_ASSERT(m_name != nullptr);
243 + WI_ASSERT(!m_name.empty());
244
245 // Ensure that no other thread is using this channel.
246 const std::unique_lock<std::mutex> lock{m_receiveMutex, std::try_to_lock};
@@ -243,7 +249,7 @@ public:
249
250 #ifdef WIN32
251
246 - THROW_HR_MSG(E_UNEXPECTED, "Incorrect channel usage detected on channel: %hs", m_name);
252 + THROW_HR_MSG(E_UNEXPECTED, "Incorrect channel usage detected on channel: %hs", m_name.c_str());
253 #else
254
255 LOG_ERROR("Incorrect channel usage detected on channel: {}", m_name);
@@ -268,7 +274,12 @@ public:
274 #ifdef WIN32
275 if (errno == HCS_E_CONNECTION_TIMEOUT)
276 {
271 - THROW_HR_MSG(HCS_E_CONNECTION_TIMEOUT, "Timeout: %u, expected type: %hs, channel: %hs", timeout, ToString(TMessage::Type), m_name);
277 + THROW_HR_MSG(
278 + HCS_E_CONNECTION_TIMEOUT,
279 + "Timeout: %u, expected type: %hs, channel: %hs",
280 + timeout,
281 + ToString(TMessage::Type),
282 + m_name.c_str());
283 }
284 #endif
285
@@ -279,7 +290,7 @@ public:
290 if (header == nullptr)
291 {
292 #ifdef WIN32
282 - THROW_HR_MSG(E_UNEXPECTED, "Message too small for header: %zd, channel: %hs", receivedSpan.size(), m_name);
293 + THROW_HR_MSG(E_UNEXPECTED, "Message too small for header: %zd, channel: %hs", receivedSpan.size(), m_name.c_str());
294 #else
295 LOG_ERROR("Message too small for header: {}, channel: {}", receivedSpan.size(), m_name);
296 THROW_ERRNO(EINVAL);
@@ -297,7 +308,7 @@ public:
308 THROW_HR_MSG(
309 E_UNEXPECTED,
310 "Unexpected transaction message received on non-transaction channel: %hs, message type: %hs",
300 - m_name,
311 + m_name.c_str(),
312 ToString(header->MessageType));
313 #else
314 LOG_ERROR(
@@ -315,7 +326,7 @@ public:
326 "Unexpected non-transaction message id: %u, expected: %u, channel: %hs",
327 header->TransactionId,
328 m_received_non_transaction_messages,
318 - m_name);
329 + m_name.c_str());
330 #else
331 LOG_ERROR("Unexpected non-transaction message id: {}, expected: {}, channel: {}", header->TransactionId, m_received_non_transaction_messages, m_name);
332 THROW_ERRNO(EINVAL);
@@ -332,7 +343,7 @@ public:
343 #ifdef WIN32
344 WSL_LOG(
345 "DiscardStaleNonTransactionMessage",
335 - TraceLoggingValue(m_name, "Name"),
346 + TraceLoggingValue(m_name.c_str(), "Name"),
347 TraceLoggingValue(ToString(header->MessageType), "MessageType"),
348 TraceLoggingValue(ToString(TMessage::Type), "ExpectedMessageType"),
349 TraceLoggingValue(header->TransactionId, "StaleNonTransactionId"),
@@ -358,7 +369,7 @@ public:
369 #ifdef WIN32
370 WSL_LOG(
371 "DiscardOutOfOrderTransactionMessage",
361 - TraceLoggingValue(m_name, "Name"),
372 + TraceLoggingValue(m_name.c_str(), "Name"),
373 TraceLoggingValue(ToString(header->MessageType), "MessageType"),
374 TraceLoggingValue(ToString(TMessage::Type), "ExpectedMessageType"),
375 TraceLoggingValue(header->TransactionStep, "StaleTransactionStep"),
@@ -385,7 +396,7 @@ public:
396 #ifdef WIN32
397 WSL_LOG(
398 "DiscardStaleTransactionMessage",
388 - TraceLoggingValue(m_name, "Name"),
399 + TraceLoggingValue(m_name.c_str(), "Name"),
400 TraceLoggingValue(ToString(header->MessageType), "MessageType"),
401 TraceLoggingValue(ToString(TMessage::Type), "ExpectedMessageType"),
402 TraceLoggingValue(header->TransactionId, "StaleTransactionId"),
@@ -407,7 +418,12 @@ public:
418 {
419 // Message is from the future.
420 #ifdef WIN32
410 - THROW_HR_MSG(E_UNEXPECTED, "Unexpected transaction message id: %u, expected: %u, channel: %hs", header->TransactionId, expectedTransactionId, m_name);
421 + THROW_HR_MSG(
422 + E_UNEXPECTED,
423 + "Unexpected transaction message id: %u, expected: %u, channel: %hs",
424 + header->TransactionId,
425 + expectedTransactionId,
426 + m_name.c_str());
427 #else
428 LOG_ERROR("Unexpected transaction message id: {}, expected: {}, channel: {}", header->TransactionId, expectedTransactionId, m_name);
429 THROW_ERRNO(EINVAL);
@@ -418,7 +434,12 @@ public:
434 {
435 // Broken transaction.
436 #ifdef WIN32
421 - THROW_HR_MSG(E_UNEXPECTED, "Unexpected transaction message step: %u, expected: %u, channel: %hs", header->TransactionStep, expectedTransactionStep, m_name);
437 + THROW_HR_MSG(
438 + E_UNEXPECTED,
439 + "Unexpected transaction message step: %u, expected: %u, channel: %hs",
440 + header->TransactionStep,
441 + expectedTransactionStep,
442 + m_name.c_str());
443 #else
444 LOG_ERROR("Unexpected transaction message step: {}, expected: {}, channel: {}", header->TransactionStep, expectedTransactionStep, m_name);
445 THROW_ERRNO(EINVAL);
@@ -434,7 +455,11 @@ public:
455 {
456 #ifdef WIN32
457 THROW_HR_MSG(
437 - E_UNEXPECTED, "Message size is too small: %zd, expected type: %hs, channel: %hs", receivedSpan.size(), ToString(TMessage::Type), m_name);
458 + E_UNEXPECTED,
459 + "Message size is too small: %zd, expected type: %hs, channel: %hs",
460 + receivedSpan.size(),
461 + ToString(TMessage::Type),
462 + m_name.c_str());
463 #else
464 LOG_ERROR("MessageSize is too small: {}, expected type: {}, channel: {}", receivedSpan.size(), ToString(TMessage::Type), m_name);
465 THROW_ERRNO(EINVAL);
@@ -445,7 +470,9 @@ public:
470
471 #ifdef WIN32
472 WSL_LOG(
448 - "ReceivedMessage", TraceLoggingValue(m_name, "Name"), TraceLoggingValue(message->PrettyPrint().c_str(), "Content"));
473 + "ReceivedMessage",
474 + TraceLoggingValue(m_name.c_str(), "Name"),
475 + TraceLoggingValue(message->PrettyPrint().c_str(), "Content"));
476 #else
477 if (LoggingEnabled())
478 {
@@ -466,7 +493,7 @@ public:
493 if (message == nullptr)
494 {
495 #ifdef WIN32
469 - THROW_HR_MSG(E_UNEXPECTED, "Expected message %hs, but socket %hs was closed", ToString(TMessage::Type), m_name);
496 + THROW_HR_MSG(E_UNEXPECTED, "Expected message %hs, but socket %hs was closed", ToString(TMessage::Type), m_name.c_str());
497 #else
498 LOG_ERROR("ExpectedMessage {}, but socket {} was closed", ToString(TMessage::Type), m_name);
499 THROW_ERRNO(EINVAL);
@@ -509,6 +536,15 @@ public:
536 return Transaction<TSentMessage>(gslhelpers::struct_as_writeable_bytes(message), responseSpan, timeout);
537 }
538
539 + template <typename TSentMessage>
540 + TSentMessage::TResponse& Transaction()
541 + {
542 + TSentMessage message{};
543 + message.Header.MessageSize = sizeof(message);
544 + message.Header.MessageType = TSentMessage::Type;
545 + return Transaction<TSentMessage>(message);
546 + }
547 +
548 void Close()
549 {
550 m_socket.reset();
@@ -519,6 +555,16 @@ public:
555 return m_socket.get();
556 }
557
558 + auto Release()
559 + {
560 + return std::move(m_socket);
561 + }
562 +
563 + bool Connected() const
564 + {
565 + return m_socket.get() >= 0;
566 + }
567 +
568 void IgnoreSequenceNumbers()
569 {
570 m_ignore_sequence = true;
@@ -566,7 +612,7 @@ private:
612 header.TransactionId,
613 header.TransactionStep,
614 expected,
569 - m_name);
615 + m_name.c_str());
616 #else
617
618 LOG_ERROR(
@@ -625,7 +671,7 @@ private:
671 uint32_t m_received_non_transaction_messages = 0;
672 std::atomic<uint32_t> m_transaction_id_seed = 0;
673 bool m_ignore_sequence = false;
628 - const char* m_name{};
674 + std::string m_name{};
675 std::mutex m_sendMutex;
676 std::mutex m_receiveMutex;
677 };
src/shared/inc/defs.h
+11
@@ -16,6 +16,17 @@ Abstract:
16
17 #if defined(_MSC_VER)
18 #define THROW_INVALID_ARG_IF(condition) THROW_HR_IF(E_INVALIDARG, condition)
19 +
20 +#define THROW_IF_FAILED_EXCEPT(result, accepted) \
21 + do \
22 + { \
23 + auto _result = (result); \
24 + if (FAILED(_result) && _result != (accepted)) \
25 + { \
26 + THROW_HR(_result); \
27 + } \
28 + } while (0)
29 +
30 #elif defined(__GNUC__)
31 #define THROW_INVALID_ARG_IF(condition) THROW_ERRNO_IF(EINVAL, condition)
32 #define _stricmp strcasecmp
src/shared/inc/lxinitshared.h
+362 -3
@@ -187,6 +187,8 @@ Abstract:
187
188 #define WSL_ROOT_INIT_ENV "WSL_ROOT_INIT"
189
190 +#define WSLC_ROOT_INIT_ENV "WSLC_ROOT_INIT"
191 +
192 #define WSL_SOCKET_LOG_ENV "WSL_SOCKET_LOG"
193
194 #define WSL_ENABLE_CRASH_DUMP_ENV "WSL_ENABLE_CRASH_DUMP"
@@ -274,6 +276,21 @@ Abstract:
276 #define INIT_NETLINK_FD_ARG "--netlink-fd"
277 #define INIT_PORT_TRACKER_LOCALHOST_RELAY "--localhost-relay"
278
279 +#define DECLARE_MESSAGE_CTOR(Name) \
280 + Name() \
281 + { \
282 + Header.MessageSize = sizeof(Name); \
283 + Header.MessageType = Name::Type; \
284 + }
285 +
286 +//
287 +// Definitions used by WSLC
288 +//
289 +
290 +constexpr auto c_ephemeralPortRange = std::pair<uint16_t, uint16_t>(10000, 20001);
291 +
292 +static_assert((c_ephemeralPortRange.first & 1) ^ (c_ephemeralPortRange.second & 1), "port range must have different parities");
293 +
294 //
295 // The types of messages that can be sent to init and mini init.
296 //
@@ -359,7 +376,30 @@ typedef enum _LX_MESSAGE_TYPE
376 LxMessageResultBool,
377 LxMessageResultInt32,
378 LxMessageResultUint32,
362 - LxMessageResultUint8
379 + LxMessageResultUint8,
380 + LxMessageWSLCMount,
381 + LxMessageWSLCMountResult,
382 + LxMessageWSLCError,
383 + LxMessageWSLCGetDisk,
384 + LxMessageWSLCGetDiskResult,
385 + LxMessageWSLCExec,
386 + LxMessageWSLCFork,
387 + LxMessageWSLCForkResult,
388 + LxMessageWSLCConnect,
389 + LxMessageWSLCAccept,
390 + LxMessageWSLCWaitPid,
391 + LxMessageWSLCWaitPidResponse,
392 + LxMessageWSLCSignal,
393 + LxMessageWSLCRelayTty,
394 + LxMessageWSLCMapPort,
395 + LxMessageWSLCConnectRelay,
396 + LxMessageWSLCPortRelay,
397 + LxMessageWSLCUnmount,
398 + LxMessageWSLCDetach,
399 + LxMessageWSLCTerminalChanged,
400 + LxMessageWSLCWatchProcesses,
401 + LxMessageWSLCProcessExited,
402 + LxMessageWSLCUnixConnect,
403 } LX_MESSAGE_TYPE,
404 *PLX_MESSAGE_TYPE;
405
@@ -448,6 +488,28 @@ inline auto ToString(LX_MESSAGE_TYPE messageType)
488 X(LxMessageResultUint32)
489 X(LxMiniInitTelemetryMessage)
490 X(LxMessageResultUint8)
491 + X(LxMessageWSLCMount)
492 + X(LxMessageWSLCMountResult)
493 + X(LxMessageWSLCGetDisk)
494 + X(LxMessageWSLCGetDiskResult)
495 + X(LxMessageWSLCExec)
496 + X(LxMessageWSLCFork)
497 + X(LxMessageWSLCForkResult)
498 + X(LxMessageWSLCConnect)
499 + X(LxMessageWSLCAccept)
500 + X(LxMessageWSLCWaitPid)
501 + X(LxMessageWSLCWaitPidResponse)
502 + X(LxMessageWSLCSignal)
503 + X(LxMessageWSLCRelayTty)
504 + X(LxMessageWSLCMapPort)
505 + X(LxMessageWSLCConnectRelay)
506 + X(LxMessageWSLCPortRelay)
507 + X(LxMessageWSLCUnmount)
508 + X(LxMessageWSLCDetach)
509 + X(LxMessageWSLCTerminalChanged)
510 + X(LxMessageWSLCWatchProcesses)
511 + X(LxMessageWSLCProcessExited)
512 + X(LxMessageWSLCUnixConnect)
513
514 default:
515 return "<unexpected LX_MESSAGE_TYPE>";
@@ -1048,13 +1110,14 @@ typedef struct _LX_INIT_START_SOCKET_RELAY
1110 {
1111 static inline auto Type = LxInitMessageStartSocketRelay;
1112
1113 + DECLARE_MESSAGE_CTOR(_LX_INIT_START_SOCKET_RELAY);
1114 +
1115 MESSAGE_HEADER Header;
1116 unsigned short Family;
1117 unsigned short Port;
1054 - int HvSocketPort;
1118 size_t BufferSize;
1119
1057 - PRETTY_PRINT(FIELD(Header), FIELD(Family), FIELD(Port), FIELD(HvSocketPort), FIELD(BufferSize));
1120 + PRETTY_PRINT(FIELD(Header), FIELD(Family), FIELD(Port), FIELD(BufferSize));
1121 } LX_INIT_START_SOCKET_RELAY, *PLX_INIT_START_SOCKET_RELAY;
1122
1123 using PCLX_INIT_START_SOCKET_RELAY = const LX_INIT_START_SOCKET_RELAY*;
@@ -1476,6 +1539,302 @@ typedef struct _LX_MINI_INIT_CREATE_INSTANCE_RESULT
1539 PRETTY_PRINT(FIELD(Header), FIELD(Result), FIELD(FailureStep), FIELD(Pid), FIELD(ConnectPort), STRING_FIELD(WarningsOffset));
1540 } LX_MINI_INIT_CREATE_INSTANCE_RESULT, *P_LX_MINI_INIT_CREATE_INSTANCE_RESULT;
1541
1542 +struct WSLC_ERROR
1543 +{
1544 + static inline auto Type = LxMessageWSLCError;
1545 + MESSAGE_HEADER Header;
1546 + int Errno{};
1547 + PRETTY_PRINT(FIELD(Header), FIELD(Errno));
1548 +};
1549 +
1550 +struct WSLC_GET_DISK_RESULT
1551 +{
1552 + static inline auto Type = LxMessageWSLCGetDiskResult;
1553 +
1554 + DECLARE_MESSAGE_CTOR(WSLC_GET_DISK_RESULT);
1555 +
1556 + MESSAGE_HEADER Header;
1557 + unsigned int Result{};
1558 + char Buffer[];
1559 +
1560 + PRETTY_PRINT(FIELD(Header), FIELD(Result), FIELD(Buffer));
1561 +};
1562 +
1563 +struct WSLC_GET_DISK
1564 +{
1565 + static inline auto Type = LxMessageWSLCGetDisk;
1566 + using TResponse = WSLC_GET_DISK_RESULT;
1567 +
1568 + DECLARE_MESSAGE_CTOR(WSLC_GET_DISK);
1569 +
1570 + MESSAGE_HEADER Header;
1571 + unsigned int ScsiLun{};
1572 +
1573 + PRETTY_PRINT(FIELD(Header), FIELD(ScsiLun));
1574 +};
1575 +
1576 +struct WSLC_MOUNT_RESULT
1577 +{
1578 + static inline auto Type = LxMessageWSLCMountResult;
1579 + MESSAGE_HEADER Header{};
1580 + int Result{};
1581 +
1582 + PRETTY_PRINT(FIELD(Header), FIELD(Result));
1583 +};
1584 +
1585 +struct WSLC_MOUNT
1586 +{
1587 + static inline auto Type = LxMessageWSLCMount;
1588 + using TResponse = WSLC_MOUNT_RESULT;
1589 +
1590 + DECLARE_MESSAGE_CTOR(WSLC_MOUNT);
1591 +
1592 + MESSAGE_HEADER Header{};
1593 + unsigned int SourceIndex{};
1594 + unsigned int DestinationIndex{};
1595 + unsigned int TypeIndex{};
1596 + unsigned int OptionsIndex{};
1597 + unsigned int Flags{};
1598 +
1599 + enum MountType : uint8_t
1600 + {
1601 + None,
1602 + ReadOnly = 1,
1603 + Chroot = 2,
1604 + OverlayFs = 4,
1605 + KernelModules = 8
1606 + };
1607 +
1608 + char Buffer[];
1609 +
1610 + PRETTY_PRINT(FIELD(Header), STRING_FIELD(SourceIndex), STRING_FIELD(DestinationIndex), STRING_FIELD(TypeIndex), STRING_FIELD(OptionsIndex));
1611 +};
1612 +
1613 +struct WSLC_EXEC
1614 +{
1615 + static inline auto Type = LxMessageWSLCExec;
1616 + using TResponse = RESULT_MESSAGE<uint32_t>;
1617 +
1618 + DECLARE_MESSAGE_CTOR(WSLC_EXEC);
1619 +
1620 + MESSAGE_HEADER Header;
1621 + unsigned int ExecutableIndex = 0;
1622 + unsigned int CommandLineIndex = 0;
1623 + unsigned int EnvironmentIndex = 0;
1624 + unsigned int CurrentDirectoryIndex = 0;
1625 + unsigned int FdIndex = 0;
1626 +
1627 + char Buffer[];
1628 +
1629 + PRETTY_PRINT(
1630 + FIELD(Header),
1631 + STRING_FIELD(ExecutableIndex),
1632 + STRING_FIELD(CurrentDirectoryIndex),
1633 + FIELD(FdIndex),
1634 + STRING_ARRAY_FIELD(CommandLineIndex),
1635 + STRING_ARRAY_FIELD(EnvironmentIndex));
1636 +};
1637 +struct WSLC_FORK_RESULT
1638 +{
1639 + static inline auto Type = LxMessageWSLCForkResult;
1640 + using TResponse = RESULT_MESSAGE<uint32_t>;
1641 +
1642 + DECLARE_MESSAGE_CTOR(WSLC_FORK_RESULT)
1643 +
1644 + MESSAGE_HEADER Header;
1645 + uint32_t Port = 0;
1646 + int32_t Pid = -1;
1647 + int32_t PtyMasterFd = -1;
1648 + PRETTY_PRINT(FIELD(Header), FIELD(Pid), FIELD(Port), FIELD(PtyMasterFd));
1649 +};
1650 +
1651 +struct WSLC_FORK
1652 +{
1653 + static inline auto Type = LxMessageWSLCFork;
1654 + using TResponse = WSLC_FORK_RESULT;
1655 +
1656 + enum ForkType : uint8_t
1657 + {
1658 + Invalid,
1659 + Process,
1660 + Thread,
1661 + Pty
1662 + };
1663 +
1664 + DECLARE_MESSAGE_CTOR(WSLC_FORK);
1665 +
1666 + MESSAGE_HEADER Header;
1667 + ForkType ForkType = Invalid;
1668 + uint16_t TtyColumns = 0;
1669 + uint16_t TtyRows = 0;
1670 + PRETTY_PRINT(FIELD(Header), FIELD(ForkType), FIELD(TtyColumns), FIELD(TtyRows));
1671 +};
1672 +
1673 +struct WSLC_TTY_RELAY
1674 +{
1675 + static inline auto Type = LxMessageWSLCRelayTty;
1676 + using TResponse = WSLC_FORK_RESULT;
1677 +
1678 + DECLARE_MESSAGE_CTOR(WSLC_TTY_RELAY);
1679 +
1680 + MESSAGE_HEADER Header;
1681 + int32_t TtyMaster{};
1682 + int32_t Socket{};
1683 + int32_t TtyControl{};
1684 +
1685 + PRETTY_PRINT(FIELD(Header), FIELD(TtyMaster), FIELD(Socket), FIELD(TtyControl));
1686 +};
1687 +
1688 +struct WSLC_ACCEPT
1689 +{
1690 + static inline auto Type = LxMessageWSLCAccept;
1691 + using TResponse = RESULT_MESSAGE<uint32_t>;
1692 + DECLARE_MESSAGE_CTOR(WSLC_ACCEPT);
1693 +
1694 + MESSAGE_HEADER Header;
1695 + int32_t Fd = -1; // TODO: multiple at once
1696 + PRETTY_PRINT(FIELD(Header), FIELD(Fd));
1697 +};
1698 +
1699 +struct WSLC_CONNECT
1700 +{
1701 + static inline auto Type = LxMessageWSLCConnect;
1702 + using TResponse = RESULT_MESSAGE<int32_t>;
1703 + DECLARE_MESSAGE_CTOR(WSLC_CONNECT);
1704 +
1705 + MESSAGE_HEADER Header;
1706 + uint32_t HostPort{};
1707 + PRETTY_PRINT(FIELD(Header), FIELD(HostPort));
1708 +};
1709 +
1710 +struct WSLC_SIGNAL
1711 +{
1712 + static inline auto Type = LxMessageWSLCSignal;
1713 + using TResponse = RESULT_MESSAGE<int32_t>;
1714 +
1715 + DECLARE_MESSAGE_CTOR(WSLC_SIGNAL);
1716 +
1717 + MESSAGE_HEADER Header;
1718 + int32_t Pid = -1;
1719 + int32_t Signal = -1;
1720 +
1721 + PRETTY_PRINT(FIELD(Header), FIELD(Pid), FIELD(Signal));
1722 +};
1723 +
1724 +struct WSLC_MAP_PORT
1725 +{
1726 + static inline auto Type = LxMessageWSLCMapPort;
1727 + using TResponse = RESULT_MESSAGE<uint32_t>;
1728 +
1729 + DECLARE_MESSAGE_CTOR(WSLC_MAP_PORT);
1730 + MESSAGE_HEADER Header{};
1731 + uint16_t WindowsPort{};
1732 + uint16_t LinuxPort{};
1733 + uint32_t AddressFamily{};
1734 + bool Stop{};
1735 +
1736 + PRETTY_PRINT(FIELD(Header));
1737 +};
1738 +
1739 +struct WSLC_CONNECT_RELAY
1740 +{
1741 + static inline auto Type = LxMessageWSLCConnectRelay;
1742 + using TResponse = RESULT_MESSAGE<uint32_t>;
1743 +
1744 + DECLARE_MESSAGE_CTOR(WSLC_CONNECT_RELAY);
1745 + MESSAGE_HEADER Header;
1746 + uint16_t Port{};
1747 + uint16_t Family{};
1748 + PRETTY_PRINT(FIELD(Header), FIELD(Port), FIELD(Family));
1749 +};
1750 +
1751 +struct WSLC_PORT_RELAY
1752 +{
1753 + static inline auto Type = LxMessageWSLCPortRelay;
1754 + using TResponse = RESULT_MESSAGE<uint32_t>;
1755 +
1756 + DECLARE_MESSAGE_CTOR(WSLC_PORT_RELAY);
1757 + MESSAGE_HEADER Header;
1758 +
1759 + PRETTY_PRINT(FIELD(Header));
1760 +};
1761 +
1762 +struct WSLC_UNMOUNT
1763 +{
1764 + static inline auto Type = LxMessageWSLCUnmount;
1765 + using TResponse = RESULT_MESSAGE<int32_t>;
1766 +
1767 + DECLARE_MESSAGE_CTOR(WSLC_UNMOUNT);
1768 + MESSAGE_HEADER Header;
1769 +
1770 + char Buffer[];
1771 +
1772 + PRETTY_PRINT(FIELD(Header), FIELD(Buffer));
1773 +};
1774 +
1775 +struct WSLC_DETACH
1776 +{
1777 + static inline auto Type = LxMessageWSLCDetach;
1778 + using TResponse = RESULT_MESSAGE<int32_t>;
1779 +
1780 + DECLARE_MESSAGE_CTOR(WSLC_DETACH);
1781 + MESSAGE_HEADER Header;
1782 + unsigned int Lun{};
1783 +
1784 + PRETTY_PRINT(FIELD(Header), FIELD(Lun));
1785 +};
1786 +
1787 +struct WSLC_TERMINAL_CHANGED
1788 +{
1789 + DECLARE_MESSAGE_CTOR(WSLC_TERMINAL_CHANGED);
1790 +
1791 + static inline auto Type = LxMessageWSLCTerminalChanged;
1792 +
1793 + MESSAGE_HEADER Header;
1794 + unsigned short Rows{};
1795 + unsigned short Columns{};
1796 +
1797 + PRETTY_PRINT(FIELD(Header), FIELD(Rows), FIELD(Columns));
1798 +};
1799 +
1800 +struct WSLC_WATCH_PROCESSES
1801 +{
1802 + DECLARE_MESSAGE_CTOR(WSLC_WATCH_PROCESSES);
1803 +
1804 + static inline auto Type = LxMessageWSLCWatchProcesses;
1805 +
1806 + MESSAGE_HEADER Header;
1807 +
1808 + PRETTY_PRINT(FIELD(Header));
1809 +};
1810 +
1811 +struct WSLC_PROCESS_EXITED
1812 +{
1813 + DECLARE_MESSAGE_CTOR(WSLC_PROCESS_EXITED);
1814 +
1815 + MESSAGE_HEADER Header;
1816 + static inline auto Type = LxMessageWSLCProcessExited;
1817 + uint32_t Pid{};
1818 + uint32_t Code{};
1819 + bool Signaled{};
1820 +
1821 + PRETTY_PRINT(FIELD(Header), FIELD(Pid), FIELD(Code), FIELD(Signaled));
1822 +};
1823 +
1824 +struct WSLC_UNIX_CONNECT
1825 +{
1826 + static inline auto Type = LxMessageWSLCUnixConnect;
1827 + using TResponse = RESULT_MESSAGE<int32_t>;
1828 +
1829 + DECLARE_MESSAGE_CTOR(WSLC_UNIX_CONNECT);
1830 +
1831 + MESSAGE_HEADER Header;
1832 + unsigned int PathOffset{};
1833 + char Buffer[];
1834 +
1835 + PRETTY_PRINT(FIELD(Header), STRING_FIELD(PathOffset));
1836 +};
1837 +
1838 typedef struct _LX_MINI_INIT_IMPORT_RESULT
1839 {
1840 static inline auto Type = LxMiniInitMessageImportResult;
src/shared/inc/message.h
+39
@@ -93,6 +93,13 @@ public:
93 gsl::copy(Span, InsertBuffer(Span.size()));
94 }
95
96 + template <typename T>
97 + gsl::span<T> InsertArray(unsigned int& Index, unsigned int& SizeInMessage, unsigned int ArraySize)
98 + {
99 + SizeInMessage = ArraySize;
100 + return InsertBuffer(Index, ArraySize * sizeof(T));
101 + }
102 +
103 gsl::span<std::byte> InsertBuffer(unsigned int& Index, size_t BufferSize, unsigned int& Size)
104 {
105 Size = BufferSize;
@@ -140,6 +147,38 @@ public:
147 WriteString(Index, wsl::shared::string::WideToMultiByte(String));
148 }
149
150 + // Write an array of strings.
151 + // Each field is prefixed with its size as int32_t, and the array ends with a -1 terminator.
152 + void WriteStringArray(unsigned int& Index, const char* const* String, size_t Count)
153 + {
154 + size_t totalSize = sizeof(int32_t); // The array ends with a '-1' terminator.
155 + for (size_t i = 0; i < Count; i++)
156 + {
157 + totalSize += strlen(String[i]) + sizeof(int32_t);
158 + }
159 +
160 + auto span = InsertBuffer(Index, totalSize);
161 + auto it = span.begin();
162 +
163 + auto insertSize = [&](int32_t size) {
164 + it = std::copy(reinterpret_cast<const std::byte*>(&size), reinterpret_cast<const std::byte*>(&size) + sizeof(size), it);
165 + };
166 +
167 + for (size_t i = 0; i < Count; i++)
168 + {
169 + auto size = strlen(String[i]);
170 + THROW_INVALID_ARG_IF(size > std::numeric_limits<int32_t>::max());
171 +
172 + insertSize(static_cast<int32_t>(size));
173 +
174 + it = std::copy(reinterpret_cast<const std::byte*>(String[i]), reinterpret_cast<const std::byte*>(String[i] + size), it);
175 + }
176 +
177 + insertSize(-1);
178 +
179 + assert(it == span.end());
180 + }
181 +
182 gsl::span<std::byte> Span()
183 {
184 // In case the structure is padded,
src/shared/inc/prettyprintshared.h
+20
@@ -33,6 +33,8 @@ Abstract:
33
34 #define STRING_FIELD(Name) #Name, (Name <= 0 ? "<empty>" : ((char*)(this)) + Name)
35
36 +#define STRING_ARRAY_FIELD(Name) #Name, (StringArray((char*)(this), Name, Header.MessageSize))
37 +
38 // Safe pretty-print for flexible array members (char Buffer[]). Bounds the read
39 // using the struct's Header.MessageSize so it never reads past the received data.
40 #define BUFFER_FIELD(Name) #Name, PrettyPrintSafeBufferView(this, Header.MessageSize, Name)
@@ -62,6 +64,13 @@ inline std::string_view PrettyPrintSafeBufferView(const void* structBase, unsign
64 return Out.str(); \
65 }
66
67 +struct StringArray
68 +{
69 + const char* MessageHead = nullptr;
70 + unsigned int Index = 0;
71 + unsigned int MessageSize = 0;
72 +};
73 +
74 template <typename T>
75 inline void PrettyPrint(std::stringstream& Out, const T& Value)
76 {
@@ -93,6 +102,17 @@ inline void PrettyPrint(std::stringstream& Out, const T& Value)
102 // N.B. Enum can be specialized by creating an overload for this method.
103 Out << std::to_string(Value);
104 }
105 + else if constexpr (std::is_same_v<T, StringArray>)
106 + {
107 + if (Value.Index <= 0)
108 + {
109 + Out << "<empty>";
110 + return;
111 + }
112 +
113 + gsl::span<const char> span(Value.MessageHead + Value.Index, Value.MessageHead + Value.MessageSize);
114 + Out << wsl::shared::string::Join(wsl::shared::string::ArrayFromSpan(gsl::as_bytes(span)), ',');
115 + }
116 else
117 {
118 Out << "{";
src/shared/inc/stringshared.h
+74
@@ -140,6 +140,64 @@ inline const char* FromSpan(gsl::span<gsl::byte> Span, size_t Offset = 0)
140 return String.data();
141 }
142
143 +template <typename T>
144 +inline const char* FromMessageBuffer(const gsl::span<gsl::byte>& Span)
145 +{
146 + return FromSpan(Span, offsetof(T, Buffer));
147 +}
148 +
149 +inline std::vector<const char*> StringPointersFromArray(const std::vector<std::string>& Strings, bool insertNull)
150 +{
151 + std::vector<const char*> result(Strings.size());
152 + std::transform(Strings.begin(), Strings.end(), result.begin(), [](const std::string& str) { return str.c_str(); });
153 +
154 + if (insertNull)
155 + {
156 + result.push_back(nullptr);
157 + }
158 +
159 + return result;
160 +}
161 +
162 +inline std::vector<std::string> ArrayFromSpan(gsl::span<const gsl::byte> Span, size_t Offset = 0)
163 +{
164 + THROW_INVALID_ARG_IF(Span.size() < Offset);
165 +
166 + Span = Span.subspan(Offset);
167 +
168 + std::vector<std::string> Result;
169 +
170 + auto it = Span.begin();
171 +
172 + auto readSize = [&]() {
173 + THROW_INVALID_ARG_IF(Span.end() - it < sizeof(int32_t));
174 +
175 + auto size = *reinterpret_cast<const int32_t*>(&*it);
176 + it += sizeof(int32_t);
177 +
178 + return size;
179 + };
180 +
181 + while (true)
182 + {
183 + auto size = readSize();
184 + if (size == -1)
185 + {
186 + break;
187 + }
188 +
189 + THROW_INVALID_ARG_IF(size < 0);
190 + THROW_INVALID_ARG_IF(size > Span.end() - it);
191 +
192 + const char* begin = reinterpret_cast<const char*>(&*it);
193 + Result.emplace_back(begin, size);
194 +
195 + it += size;
196 + }
197 +
198 + return Result;
199 +}
200 +
201 constexpr auto c_defaultHostName = "localhost";
202
203 inline std::string CleanHostname(const std::string_view Hostname)
@@ -832,6 +890,22 @@ struct std::formatter<std::source_location, char>
890 }
891 };
892
893 +template <>
894 +struct std::formatter<std::source_location, wchar_t>
895 +{
896 + template <typename TCtx>
897 + static constexpr auto parse(TCtx& ctx)
898 + {
899 + return ctx.begin();
900 + }
901 +
902 + template <typename TCtx>
903 + auto format(const std::source_location& location, TCtx& ctx) const
904 + {
905 + return std::format_to(ctx.out(), L"{}[{}:{}]", location.function_name(), location.file_name(), location.line());
906 + }
907 +};
908 +
909 template <>
910 struct std::formatter<char*, wchar_t>
911 {
src/windows/WslcSDK/CMakeLists.txt new
+21
@@ -0,0 +1,21 @@
1 +set(SOURCES
2 + IOCallback.cpp
3 + ProgressCallback.cpp
4 + TerminationCallback.cpp
5 + wslcsdk.cpp
6 + WslcsdkPrivate.cpp
7 +)
8 +set(HEADERS
9 + IOCallback.h
10 + ProgressCallback.h
11 + TerminationCallback.h
12 + wslcsdk.h
13 + WslcsdkPrivate.h
14 +)
15 +
16 +add_library(wslcsdk SHARED ${SOURCES} ${HEADERS} wslcsdk.def)
17 +set_target_properties(wslcsdk PROPERTIES EXCLUDE_FROM_ALL FALSE)
18 +add_dependencies(wslcsdk wslserviceidl)
19 +target_link_libraries(wslcsdk ${COMMON_LINK_LIBRARIES} legacy_stdio_definitions common)
20 +target_precompile_headers(wslcsdk REUSE_FROM common)
21 +set_target_properties(wslcsdk PROPERTIES FOLDER windows)
src/windows/WslcSDK/IOCallback.cpp new
+112
@@ -0,0 +1,112 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + IOCallback.cpp
8 +
9 +Abstract:
10 +
11 + Holds IO callback objects.
12 +
13 +--*/
14 +#include "precomp.h"
15 +#include "WslcsdkPrivate.h"
16 +
17 +IOCallback::IOCallback(IWSLCProcess* process, const WslcContainerProcessIOCallbackOptions& options) :
18 + m_process(process), m_callbackOptions(std::make_unique<WslcContainerProcessIOCallbackOptions>(options))
19 +{
20 + using namespace wsl::windows::common::relay;
21 +
22 + auto addIOCallback = [&](WslcProcessIOHandle ioHandle, WslcStdIOCallback callback, PVOID context) {
23 + std::function<void(const gsl::span<char>& Buffer)> function;
24 + if (callback)
25 + {
26 + function = [ioHandle, callback, context](const gsl::span<char>& buffer) {
27 + callback(ioHandle, reinterpret_cast<const BYTE*>(buffer.data()), static_cast<uint32_t>(buffer.size()), context);
28 + };
29 + }
30 + else
31 + {
32 + function = [](const gsl::span<char>&) {};
33 + }
34 +
35 + m_io.AddHandle(std::make_unique<ReadHandle>(GetIOHandle(process, ioHandle), std::move(function)));
36 + };
37 +
38 + addIOCallback(WSLC_PROCESS_IO_HANDLE_STDOUT, options.onStdOut, options.callbackContext);
39 + addIOCallback(WSLC_PROCESS_IO_HANDLE_STDERR, options.onStdErr, options.callbackContext);
40 +
41 + if (options.onExit)
42 + {
43 + wil::unique_handle processExitEvent;
44 + THROW_IF_FAILED(process->GetExitEvent(&processExitEvent));
45 + m_io.AddHandle(std::make_unique<EventHandle>(std::move(processExitEvent)));
46 + }
47 +
48 + m_io.AddHandle(std::make_unique<EventHandle>(m_cancelEvent.get()), MultiHandleWait::CancelOnCompleted | MultiHandleWait::NeedNotComplete);
49 +
50 + m_thread = std::thread([this]() {
51 + try
52 + {
53 + // Will be false when cancelled.
54 + bool runResult = m_io.Run({});
55 +
56 + if (runResult && m_process && m_callbackOptions && m_callbackOptions->onExit)
57 + {
58 + WSLCProcessState state{};
59 + int exitCode = -1;
60 +
61 + // Prefer to make the callback even if we don't properly retrieve the exit code.
62 + if (FAILED_LOG(m_process->GetState(&state, &exitCode)))
63 + {
64 + // Reset to our known value in case GetState stomped it while failing.
65 + exitCode = -1;
66 + }
67 + else
68 + {
69 + WI_ASSERT(state == WslcProcessStateExited);
70 + }
71 +
72 + // Regardless of our ability to get the proper exit code, inform the caller that the process
73 + // has exited and they will not be getting any additional IO callbacks.
74 + m_callbackOptions->onExit(exitCode, m_callbackOptions->callbackContext);
75 + }
76 + }
77 + CATCH_LOG();
78 + });
79 +}
80 +
81 +IOCallback::~IOCallback()
82 +{
83 + Cancel();
84 + if (m_thread.joinable())
85 + {
86 + m_thread.join();
87 + }
88 +}
89 +
90 +void IOCallback::Cancel()
91 +{
92 + m_cancelEvent.SetEvent();
93 +}
94 +
95 +bool IOCallback::HasIOCallback(const WslcContainerProcessOptionsInternal* options)
96 +{
97 + return options && HasIOCallback(options->ioCallbacks);
98 +}
99 +
100 +bool IOCallback::HasIOCallback(const WslcContainerProcessIOCallbackOptions& options)
101 +{
102 + return options.onStdOut || options.onStdErr || options.onExit;
103 +}
104 +
105 +wil::unique_handle IOCallback::GetIOHandle(IWSLCProcess* process, WslcProcessIOHandle ioHandle)
106 +{
107 + wsl::windows::common::wslutil::COMOutputHandle handle;
108 +
109 + THROW_IF_FAILED(process->GetStdHandle(static_cast<WSLCFD>(static_cast<std::underlying_type_t<WslcProcessIOHandle>>(ioHandle)), &handle));
110 +
111 + return handle.Release();
112 +}
src/windows/WslcSDK/IOCallback.h new
+40
@@ -0,0 +1,40 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + IOCallback.h
8 +
9 +Abstract:
10 +
11 + Holds IO callback objects.
12 +
13 +--*/
14 +#pragma once
15 +#include "wslc.h"
16 +#include "relay.hpp"
17 +#include <thread>
18 +
19 +struct WslcContainerProcessIOCallbackOptions;
20 +struct WslcContainerProcessOptionsInternal;
21 +
22 +struct IOCallback
23 +{
24 + IOCallback(IWSLCProcess* process, const WslcContainerProcessIOCallbackOptions& options);
25 + ~IOCallback();
26 +
27 + void Cancel();
28 +
29 + static bool HasIOCallback(const WslcContainerProcessOptionsInternal* options);
30 + static bool HasIOCallback(const WslcContainerProcessIOCallbackOptions& options);
31 +
32 + static wil::unique_handle GetIOHandle(IWSLCProcess* process, WslcProcessIOHandle ioHandle);
33 +
34 +private:
35 + wil::com_ptr<IWSLCProcess> m_process;
36 + std::unique_ptr<WslcContainerProcessIOCallbackOptions> m_callbackOptions;
37 + std::thread m_thread;
38 + wsl::windows::common::relay::MultiHandleWait m_io;
39 + wil::unique_event m_cancelEvent{wil::EventOptions::ManualReset};
40 +};
src/windows/WslcSDK/ProgressCallback.cpp new
+62
@@ -0,0 +1,62 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ProgressCallback.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of a type that implements IProgressCallback.
12 +
13 +--*/
14 +#include "precomp.h"
15 +#include "ProgressCallback.h"
16 +
17 +using namespace std::string_view_literals;
18 +
19 +namespace {
20 +WslcImageProgressStatus ConvertStatus(LPCSTR Status)
21 +{
22 +#define WSLC_STRING_TO_STATUS_MAPPING(_status_, _string_) \
23 + if (_string_##sv == Status) \
24 + { \
25 + return _status_; \
26 + }
27 +
28 + // TODO: Mapping engine strings to status values seems fragile.
29 + // WSLC is intentionally avoiding this kind of thing for localization of engine strings, which amounts to the same
30 + // thing. If we keep this, a test should be added to explicitly validate that each status is returned properly.
31 + WSLC_STRING_TO_STATUS_MAPPING(WSLC_IMAGE_PROGRESS_STATUS_PULLING, "Pulling fs layer");
32 + WSLC_STRING_TO_STATUS_MAPPING(WSLC_IMAGE_PROGRESS_STATUS_WAITING, "Waiting");
33 + WSLC_STRING_TO_STATUS_MAPPING(WSLC_IMAGE_PROGRESS_STATUS_DOWNLOADING, "Downloading");
34 + WSLC_STRING_TO_STATUS_MAPPING(WSLC_IMAGE_PROGRESS_STATUS_VERIFYING, "Verifying Checksum");
35 + WSLC_STRING_TO_STATUS_MAPPING(WSLC_IMAGE_PROGRESS_STATUS_EXTRACTING, "Extracting");
36 + WSLC_STRING_TO_STATUS_MAPPING(WSLC_IMAGE_PROGRESS_STATUS_COMPLETE, "Pull complete");
37 +
38 + return WSLC_IMAGE_PROGRESS_STATUS_UNKNOWN;
39 +}
40 +} // namespace
41 +
42 +ProgressCallback::ProgressCallback(WslcContainerImageProgressCallback callback, PVOID context) :
43 + m_callback(callback), m_context(context)
44 +{
45 +}
46 +
47 +HRESULT STDMETHODCALLTYPE ProgressCallback::OnProgress(LPCSTR Status, LPCSTR Id, ULONGLONG Current, ULONGLONG Total)
48 +{
49 + if (m_callback)
50 + {
51 + WslcImageProgressMessage message{};
52 +
53 + message.id = Id;
54 + message.status = ConvertStatus(Status);
55 + message.detail.currentBytes = Current;
56 + message.detail.totalBytes = Total;
57 +
58 + return m_callback(&message, m_context);
59 + }
60 +
61 + return S_OK;
62 +}
src/windows/WslcSDK/ProgressCallback.h new
+43
@@ -0,0 +1,43 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ProgressCallback.h
8 +
9 +Abstract:
10 +
11 + Header for a type that implements IProgressCallback.
12 +
13 +--*/
14 +#pragma once
15 +#include "wslc.h"
16 +#include "wslcsdkprivate.h"
17 +#include <winrt/base.h>
18 +
19 +struct ProgressCallback : public winrt::implements<ProgressCallback, IProgressCallback>
20 +{
21 + ProgressCallback(WslcContainerImageProgressCallback callback, PVOID context);
22 +
23 + // IProgressCallback
24 + HRESULT STDMETHODCALLTYPE OnProgress(LPCSTR Status, LPCSTR Id, ULONGLONG Current, ULONGLONG Total) override;
25 +
26 + // Creates a ProgressCallback if the options provides a callback.
27 + template <typename Options>
28 + static winrt::com_ptr<ProgressCallback> CreateIf(const Options* options)
29 + {
30 + if (options && options->progressCallback)
31 + {
32 + return winrt::make_self<ProgressCallback>(options->progressCallback, options->progressCallbackContext);
33 + }
34 + else
35 + {
36 + return nullptr;
37 + }
38 + }
39 +
40 +private:
41 + WslcContainerImageProgressCallback m_callback = nullptr;
42 + PVOID m_context = nullptr;
43 +};
src/windows/WslcSDK/TerminationCallback.cpp new
+58
@@ -0,0 +1,58 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + TerminationCallback.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of a type that implements ITerminationCallback.
12 +
13 +--*/
14 +#include "precomp.h"
15 +#include "TerminationCallback.h"
16 +
17 +namespace {
18 +WslcSessionTerminationReason ConvertReason(WSLCVirtualMachineTerminationReason Reason)
19 +{
20 + switch (Reason)
21 + {
22 + case WSLCVirtualMachineTerminationReasonShutdown:
23 + return WSLC_SESSION_TERMINATION_REASON_SHUTDOWN;
24 + case WSLCVirtualMachineTerminationReasonCrashed:
25 + return WSLC_SESSION_TERMINATION_REASON_CRASHED;
26 + default:
27 + return WSLC_SESSION_TERMINATION_REASON_UNKNOWN;
28 + }
29 +}
30 +} // namespace
31 +
32 +TerminationCallback::TerminationCallback(WslcSessionTerminationCallback callback, PVOID context) :
33 + m_callback(callback), m_context(context)
34 +{
35 +}
36 +
37 +// TODO: Details from the runtime are dropped; should the SDK callback function be updated to include the reasons string?
38 +HRESULT STDMETHODCALLTYPE TerminationCallback::OnTermination(WSLCVirtualMachineTerminationReason Reason, LPCWSTR)
39 +{
40 + if (m_callback)
41 + {
42 + m_callback(ConvertReason(Reason), m_context);
43 + }
44 +
45 + return S_OK;
46 +}
47 +
48 +winrt::com_ptr<TerminationCallback> TerminationCallback::CreateIf(const WslcSessionOptionsInternal* options)
49 +{
50 + if (options->terminationCallback)
51 + {
52 + return winrt::make_self<TerminationCallback>(options->terminationCallback, options->terminationCallbackContext);
53 + }
54 + else
55 + {
56 + return nullptr;
57 + }
58 +}
src/windows/WslcSDK/TerminationCallback.h new
+32
@@ -0,0 +1,32 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + TerminationCallback.h
8 +
9 +Abstract:
10 +
11 + Header for a type that implements ITerminationCallback.
12 +
13 +--*/
14 +#pragma once
15 +#include "wslc.h"
16 +#include "wslcsdkprivate.h"
17 +#include <winrt/base.h>
18 +
19 +struct TerminationCallback : public winrt::implements<TerminationCallback, ITerminationCallback>
20 +{
21 + TerminationCallback(WslcSessionTerminationCallback callback, PVOID context);
22 +
23 + // ITerminationCallback
24 + HRESULT STDMETHODCALLTYPE OnTermination(WSLCVirtualMachineTerminationReason Reason, LPCWSTR Details) override;
25 +
26 + // Creates a TerminationCallback if the options provides a callback.
27 + static winrt::com_ptr<TerminationCallback> CreateIf(const WslcSessionOptionsInternal* options);
28 +
29 +private:
30 + WslcSessionTerminationCallback m_callback = nullptr;
31 + PVOID m_context = nullptr;
32 +};
src/windows/WslcSDK/WslcsdkPrivate.cpp new
+51
@@ -0,0 +1,51 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WslcSDKPrivate.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the private WSL Container SDK implementations.
12 +
13 +--*/
14 +#include "precomp.h"
15 +
16 +#include "wslcsdkprivate.h"
17 +
18 +WslcSessionOptionsInternal* GetInternalType(WslcSessionSettings* settings)
19 +{
20 + return reinterpret_cast<WslcSessionOptionsInternal*>(settings);
21 +}
22 +
23 +WslcContainerProcessOptionsInternal* GetInternalType(WslcProcessSettings* settings)
24 +{
25 + return reinterpret_cast<WslcContainerProcessOptionsInternal*>(settings);
26 +}
27 +
28 +WslcContainerOptionsInternal* GetInternalType(WslcContainerSettings* settings)
29 +{
30 + return reinterpret_cast<WslcContainerOptionsInternal*>(settings);
31 +}
32 +
33 +const WslcContainerOptionsInternal* GetInternalType(const WslcContainerSettings* settings)
34 +{
35 + return reinterpret_cast<const WslcContainerOptionsInternal*>(settings);
36 +}
37 +
38 +WslcSessionImpl* GetInternalType(WslcSession handle)
39 +{
40 + return reinterpret_cast<WslcSessionImpl*>(handle);
41 +}
42 +
43 +WslcContainerImpl* GetInternalType(WslcContainer handle)
44 +{
45 + return reinterpret_cast<WslcContainerImpl*>(handle);
46 +}
47 +
48 +WslcProcessImpl* GetInternalType(WslcProcess handle)
49 +{
50 + return reinterpret_cast<WslcProcessImpl*>(handle);
51 +}
src/windows/WslcSDK/WslcsdkPrivate.h new
+146
@@ -0,0 +1,146 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WslcSDKPrivate.h
8 +
9 +Abstract:
10 +
11 + This file contains the private WSL Container SDK definitions.
12 +
13 +--*/
14 +#pragma once
15 +#include <windows.h>
16 +#include "wslcsdk.h"
17 +#include "wslc.h"
18 +#include "IOCallback.h"
19 +#include <stdint.h>
20 +#include <wil/com.h> // COM helpers
21 +// #include <wil/resource.h> // handle wrappers
22 +// #include <wil/result.h> // error handling
23 +
24 +// SESSION DEFINITIONS
25 +typedef struct WslcSessionOptionsInternal
26 +{
27 + PCWSTR displayName;
28 + PCWSTR storagePath;
29 +
30 + uint32_t cpuCount;
31 + uint32_t memoryMb;
32 + uint32_t timeoutMS;
33 +
34 + WslcVhdRequirements vhdRequirements;
35 + WslcSessionFeatureFlags featureFlags;
36 + WslcSessionTerminationCallback terminationCallback;
37 + PVOID terminationCallbackContext;
38 +} WslcSessionOptionsInternal;
39 +
40 +static_assert(sizeof(WslcSessionOptionsInternal) == WSLC_SESSION_OPTIONS_SIZE, "WSLC_SESSION_OPTIONS_INTERNAL size mismatch");
41 +
42 +static_assert(
43 + __alignof(WslcSessionOptionsInternal) == WSLC_SESSION_OPTIONS_ALIGNMENT, "WSLC_SESSION_OPTIONS_INTERNAL alignment mismatch");
44 +
45 +static_assert(std::is_trivial_v<WslcSessionOptionsInternal>, "WSLC_SESSION_OPTIONS_INTERNAL must be trivial");
46 +
47 +WslcSessionOptionsInternal* GetInternalType(WslcSessionSettings* settings);
48 +
49 +struct WslcContainerProcessIOCallbackOptions : public WslcProcessCallbacks
50 +{
51 + PVOID callbackContext;
52 +};
53 +
54 +// PROCESS DEFINITIONS
55 +typedef struct WslcContainerProcessOptionsInternal
56 +{
57 + PCSTR const* commandLine;
58 + uint32_t commandLineCount;
59 + PCSTR const* environment;
60 + uint32_t environmentCount;
61 + PCSTR workingDirectory;
62 + WslcContainerProcessIOCallbackOptions ioCallbacks;
63 +} WslcContainerProcessOptionsInternal;
64 +
65 +static_assert(
66 + sizeof(WslcContainerProcessOptionsInternal) == WSLC_CONTAINER_PROCESS_OPTIONS_SIZE,
67 + "WSLC_CONTAINER_PROCESS_OPTIONS_INTERNAL size mismatch");
68 +static_assert(
69 + __alignof(WslcContainerProcessOptionsInternal) == WSLC_CONTAINER_PROCESS_OPTIONS_ALIGNMENT,
70 + "WSLC_CONTAINER_PROCESS_OPTIONS_INTERNAL alignment mismatch");
71 +
72 +static_assert(std::is_trivial_v<WslcContainerProcessOptionsInternal>, "WSLC_CONTAINER_PROCESS_OPTIONS_INTERNAL must be trivial");
73 +
74 +WslcContainerProcessOptionsInternal* GetInternalType(WslcProcessSettings* settings);
75 +
76 +// CONTAINER DEFINITIONS
77 +typedef struct WslcContainerOptionsInternal
78 +{
79 + PCSTR image; // Image name (repository:tag)
80 + PCSTR runtimeName; // Container runtime name (expected to allow DNS resolution between containers)
81 + PCSTR HostName;
82 + PCSTR DomainName;
83 + const WslcContainerPortMapping* ports;
84 + uint32_t portsCount;
85 + const WslcContainerVolume* volumes;
86 + uint32_t volumesCount;
87 + const WslcContainerNamedVolume* namedVolumes;
88 + uint32_t namedVolumesCount;
89 + const WslcContainerProcessOptionsInternal* initProcessOptions;
90 + WSLCContainerNetworkType networking;
91 + WslcContainerFlags containerFlags;
92 +
93 +} WslcContainerOptionsInternal;
94 +
95 +static_assert(
96 + sizeof(WslcContainerOptionsInternal) == WSLC_CONTAINER_OPTIONS_SIZE, "WSLC_CONTAINER_OPTIONS_INTERNAL size mismatch");
97 +static_assert(
98 + __alignof(WslcContainerOptionsInternal) == WSLC_CONTAINER_OPTIONS_ALIGNMENT,
99 + "WSLC_CONTAINER_OPTIONS_INTERNAL alignment mismatch");
100 +
101 +static_assert(std::is_trivial_v<WslcContainerOptionsInternal>, "WSLC_CONTAINER_OPTIONS_INTERNAL must be trivial");
102 +
103 +WslcContainerOptionsInternal* GetInternalType(WslcContainerSettings* settings);
104 +const WslcContainerOptionsInternal* GetInternalType(const WslcContainerSettings* settings);
105 +
106 +// Use to allocate the actual objects on the heap to keep it alive.
107 +struct WslcSessionImpl
108 +{
109 + wil::com_ptr<IWSLCSession> session;
110 + wil::com_ptr<ITerminationCallback> terminationCallback;
111 +};
112 +
113 +WslcSessionImpl* GetInternalType(WslcSession handle);
114 +
115 +struct WslcContainerImpl
116 +{
117 + wil::com_ptr<IWSLCContainer> container;
118 + WslcContainerProcessIOCallbackOptions ioCallbackOptions{};
119 + std::atomic<std::shared_ptr<IOCallback>> ioCallbacks;
120 +};
121 +
122 +WslcContainerImpl* GetInternalType(WslcContainer handle);
123 +
124 +struct WslcProcessImpl
125 +{
126 + wil::com_ptr<IWSLCProcess> process;
127 + std::shared_ptr<IOCallback> ioCallbacks;
128 +};
129 +
130 +WslcProcessImpl* GetInternalType(WslcProcess handle);
131 +
132 +// Converts to the internal type and throws an error on null input.
133 +template <typename T>
134 +auto CheckAndGetInternalType(T* value)
135 +{
136 + THROW_HR_IF_NULL(E_POINTER, value);
137 + return GetInternalType(value);
138 +}
139 +
140 +// Converts to the internal type and throws an error on null input.
141 +template <typename T>
142 +auto CheckAndGetInternalTypeUniquePointer(T* value)
143 +{
144 + THROW_HR_IF_NULL(E_POINTER, value);
145 + return std::unique_ptr<std::remove_pointer_t<decltype(GetInternalType(value))>>{GetInternalType(value)};
146 +}
src/windows/WslcSDK/csharp/CMakeLists.txt new
+13
@@ -0,0 +1,13 @@
1 +enable_language(CSharp)
2 +
3 +add_library(wslcsdkcs SHARED "Projection.cs")
4 +configure_csharp_target(wslcsdkcs)
5 +
6 +set_target_properties(
7 + wslcsdkcs PROPERTIES
8 + FOLDER windows
9 + LINKER_LANGUAGE CSharp
10 + VS_GLOBAL_PlatformTarget "AnyCPU"
11 +)
12 +
13 +add_dependencies(wslcsdkcs wslcsdk)
src/windows/WslcSDK/csharp/Projection.cs new
+8
@@ -0,0 +1,8 @@
1 +namespace Microsoft.WSL.Containers
2 +{
3 + // This is only a placeholder so that we can compile a DLL.
4 + // The actual implementation of the projection will be added later.
5 + internal class Projection
6 + {
7 + }
8 +}
\ No newline at end of file
src/windows/WslcSDK/wslcsdk.cpp new
+1582
@@ -0,0 +1,1582 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + wslcsdk.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the public WSLC Client SDK api implementations.
12 +
13 +--*/
14 +#include "precomp.h"
15 +
16 +#include "wslcsdk.h"
17 +#include "WslcsdkPrivate.h"
18 +#include "ProgressCallback.h"
19 +#include "TerminationCallback.h"
20 +#include "Localization.h"
21 +#include "WslInstall.h"
22 +#include "wslutil.h"
23 +#include "WindowsUpdateIntegration.h"
24 +
25 +using namespace std::string_view_literals;
26 +using namespace wsl::windows::common::wslutil;
27 +
28 +namespace {
29 +constexpr uint32_t s_DefaultCPUCount = 2;
30 +constexpr uint32_t s_DefaultMemoryMB = 2000;
31 +// Maximum value per use with HVSOCKET_CONNECT_TIMEOUT_MAX
32 +constexpr ULONG s_DefaultBootTimeout = 300000;
33 +// Default to 1 GB
34 +constexpr UINT64 s_DefaultStorageSize = 1000 * 1000 * 1000;
35 +
36 +#define WSLC_FLAG_VALUE_ASSERT(_wlsc_name_, _wslc_name_) \
37 + static_assert(_wlsc_name_ == _wslc_name_, "Flag values differ: " #_wlsc_name_ " != " #_wslc_name_);
38 +
39 +template <typename Flags>
40 +struct FlagsTraits
41 +{
42 + static_assert(false, "Flags used without traits defined.");
43 +};
44 +
45 +template <>
46 +struct FlagsTraits<WslcSessionFeatureFlags>
47 +{
48 + using WslcType = WSLCFeatureFlags;
49 + constexpr static WslcSessionFeatureFlags Mask = WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU;
50 + WSLC_FLAG_VALUE_ASSERT(WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU, WslcFeatureFlagsGPU);
51 +};
52 +
53 +template <>
54 +struct FlagsTraits<WslcContainerFlags>
55 +{
56 + using WslcType = WSLCContainerFlags;
57 + constexpr static WslcContainerFlags Mask = WSLC_CONTAINER_FLAG_AUTO_REMOVE | WSLC_CONTAINER_FLAG_ENABLE_GPU;
58 + WSLC_FLAG_VALUE_ASSERT(WSLC_CONTAINER_FLAG_AUTO_REMOVE, WSLCContainerFlagsRm);
59 + WSLC_FLAG_VALUE_ASSERT(WSLC_CONTAINER_FLAG_ENABLE_GPU, WSLCContainerFlagsGpu);
60 + // TODO: WSLC_CONTAINER_FLAG_PRIVILEGED has no associated runtime value
61 +};
62 +
63 +template <>
64 +struct FlagsTraits<WslcContainerStartFlags>
65 +{
66 + using WslcType = WSLCContainerStartFlags;
67 + constexpr static WslcContainerStartFlags Mask = WSLC_CONTAINER_START_FLAG_ATTACH;
68 + WSLC_FLAG_VALUE_ASSERT(WSLC_CONTAINER_START_FLAG_ATTACH, WSLCContainerStartFlagsAttach);
69 +};
70 +
71 +template <>
72 +struct FlagsTraits<WslcDeleteContainerFlags>
73 +{
74 + using WslcType = WSLCDeleteFlags;
75 + constexpr static WslcDeleteContainerFlags Mask = WSLC_DELETE_CONTAINER_FLAG_FORCE;
76 + WSLC_FLAG_VALUE_ASSERT(WSLC_DELETE_CONTAINER_FLAG_FORCE, WSLCDeleteFlagsForce);
77 +};
78 +
79 +template <typename Flags>
80 +typename FlagsTraits<Flags>::WslcType ConvertFlags(Flags flags)
81 +{
82 + using traits = FlagsTraits<Flags>;
83 + return static_cast<typename traits::WslcType>(flags & traits::Mask);
84 +}
85 +
86 +WSLCSignal Convert(WslcSignal signal)
87 +{
88 + switch (signal)
89 + {
90 + case WSLC_SIGNAL_NONE:
91 + return WSLCSignal::WSLCSignalNone;
92 + case WSLC_SIGNAL_SIGHUP:
93 + return WSLCSignal::WSLCSignalSIGHUP;
94 + case WSLC_SIGNAL_SIGINT:
95 + return WSLCSignal::WSLCSignalSIGINT;
96 + case WSLC_SIGNAL_SIGQUIT:
97 + return WSLCSignal::WSLCSignalSIGQUIT;
98 + case WSLC_SIGNAL_SIGKILL:
99 + return WSLCSignal::WSLCSignalSIGKILL;
100 + case WSLC_SIGNAL_SIGTERM:
101 + return WSLCSignal::WSLCSignalSIGTERM;
102 + default:
103 + THROW_HR_MSG(E_INVALIDARG, "Invalid WslcSignal: %i", signal);
104 + }
105 +}
106 +
107 +WSLCContainerNetworkType Convert(WslcContainerNetworkingMode mode)
108 +{
109 + switch (mode)
110 + {
111 + case WSLC_CONTAINER_NETWORKING_MODE_NONE:
112 + return WSLCContainerNetworkTypeNone;
113 + case WSLC_CONTAINER_NETWORKING_MODE_BRIDGED:
114 + return WSLCContainerNetworkTypeBridged;
115 + default:
116 + THROW_HR_MSG(E_INVALIDARG, "Invalid WslcContainerNetworkingMode: %i", mode);
117 + }
118 +}
119 +
120 +void ConvertSHA256Hash(const char* hashString, uint8_t sha256[32])
121 +{
122 + static constexpr std::string_view s_sha256Prefix = "sha256:"sv;
123 + static constexpr size_t s_sha256ByteCount = 32;
124 +
125 + THROW_HR_IF_NULL(E_POINTER, sha256);
126 +
127 + if (!hashString)
128 + {
129 + return;
130 + }
131 +
132 + std::string_view hashStringView{hashString};
133 + THROW_HR_IF_MSG(
134 + E_UNEXPECTED,
135 + hashStringView.length() < s_sha256Prefix.length() || hashStringView.substr(0, s_sha256Prefix.length()) != s_sha256Prefix,
136 + "Unexpected hash specifier: %hs",
137 + hashString);
138 +
139 + auto hashBytes = wsl::windows::common::string::HexToBytes(hashStringView.substr(s_sha256Prefix.length()));
140 + THROW_HR_IF_MSG(E_INVALIDARG, hashBytes.size() != s_sha256ByteCount, "SHA256 hash was not 32 bytes: %zu", hashBytes.size());
141 + memcpy(sha256, &hashBytes[0], s_sha256ByteCount);
142 +}
143 +
144 +// TODO: Replace with a derivation of wsl::windows::common::ExecutionContext when telemetry changes are introduced
145 +// This will make usage even easier as we can just use the WIL result macros directly.
146 +struct ErrorInfoWrapper
147 +{
148 + ErrorInfoWrapper(PWSTR* errorMessage) : m_errorMessage(errorMessage)
149 + {
150 + if (m_errorMessage)
151 + {
152 + *m_errorMessage = nullptr;
153 + }
154 + }
155 +
156 + void GetErrorInfoFromCOM()
157 + {
158 + if (m_errorMessage)
159 + {
160 + auto errorInfo = wsl::windows::common::wslutil::GetCOMErrorInfo();
161 + if (errorInfo)
162 + {
163 + *m_errorMessage = wil::make_unique_string<wil::unique_cotaskmem_string>(errorInfo->Message.get()).release();
164 + }
165 + }
166 + }
167 +
168 + HRESULT CaptureResult(HRESULT hr)
169 + {
170 + m_hr = hr;
171 + if (FAILED_LOG(m_hr.value()))
172 + {
173 + GetErrorInfoFromCOM();
174 + }
175 + return m_hr.value();
176 + }
177 +
178 + operator HRESULT() const
179 + {
180 + THROW_HR_IF(E_UNEXPECTED, !m_hr);
181 + return m_hr.value();
182 + }
183 +
184 +private:
185 + PWSTR* m_errorMessage = nullptr;
186 + std::optional<HRESULT> m_hr;
187 +};
188 +
189 +void EnsureAbsolutePath(const std::filesystem::path& path, bool containerPath)
190 +{
191 + THROW_HR_IF(E_INVALIDARG, path.empty());
192 +
193 + if (containerPath)
194 + {
195 + auto pathString = path.native();
196 + // Not allowed to mount to root
197 + THROW_HR_IF(E_INVALIDARG, pathString.length() < 2);
198 + // Must be absolute
199 + THROW_HR_IF(E_INVALIDARG, pathString[0] != L'/');
200 + }
201 + else
202 + {
203 + THROW_HR_IF(E_INVALIDARG, path.is_relative());
204 + }
205 +}
206 +static HRESULT InetNtopToHresult(int af, const void* src, char* dst, size_t dstCount)
207 +{
208 + if (inet_ntop(af, src, dst, dstCount) == nullptr)
209 + {
210 + return HRESULT_FROM_WIN32(WSAGetLastError());
211 + }
212 + return S_OK;
213 +}
214 +
215 +bool CopyProcessSettingsToRuntime(WSLCProcessOptions& runtimeOptions, const WslcContainerProcessOptionsInternal* initProcessOptions)
216 +{
217 + if (initProcessOptions)
218 + {
219 + runtimeOptions.CurrentDirectory = initProcessOptions->workingDirectory;
220 + runtimeOptions.CommandLine.Values = initProcessOptions->commandLine;
221 + runtimeOptions.CommandLine.Count = initProcessOptions->commandLineCount;
222 + runtimeOptions.Environment.Values = initProcessOptions->environment;
223 + runtimeOptions.Environment.Count = initProcessOptions->environmentCount;
224 +
225 + // TODO: No user access
226 + // containerOptions.InitProcessOptions.Flags;
227 + // containerOptions.InitProcessOptions.TtyRows;
228 + // containerOptions.InitProcessOptions.TtyColumns;
229 + // containerOptions.InitProcessOptions.User;
230 +
231 + return true;
232 + }
233 + else
234 + {
235 + return false;
236 + }
237 +}
238 +
239 +// Normalizes file inputs to HANDLE+length.
240 +struct ImageFileResolver
241 +{
242 + ImageFileResolver(PCWSTR path) : m_fileHandle(INVALID_HANDLE_VALUE)
243 + {
244 + THROW_HR_IF_NULL(E_POINTER, path);
245 +
246 + wil::unique_handle imageFileHandle{
247 + CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
248 + THROW_LAST_ERROR_IF(!imageFileHandle);
249 +
250 + LARGE_INTEGER fileSize{};
251 + THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(imageFileHandle.get(), &fileSize));
252 +
253 + m_fileHandle = std::move(imageFileHandle);
254 + m_length = static_cast<ULONGLONG>(fileSize.QuadPart);
255 + }
256 +
257 + ImageFileResolver(HANDLE imageContent, uint64_t imageContentLength) : m_fileHandle(imageContent)
258 + {
259 + THROW_HR_IF(E_INVALIDARG, imageContent == nullptr || imageContent == INVALID_HANDLE_VALUE);
260 + THROW_HR_IF(E_INVALIDARG, imageContentLength == 0);
261 +
262 + m_length = imageContentLength;
263 + }
264 +
265 + HANDLE Handle() const
266 + {
267 + return m_fileHandle.Get();
268 + }
269 +
270 + ULONGLONG Length() const
271 + {
272 + return m_length;
273 + }
274 +
275 +private:
276 + wsl::windows::common::relay::HandleWrapper m_fileHandle;
277 + ULONGLONG m_length;
278 +};
279 +
280 +// TODO: Implement Server SKU specific checks
281 +bool NeedsVirtualMachineServicesInstalled()
282 +{
283 + return !wsl::windows::common::wslutil::IsVirtualMachinePlatformInstalled();
284 +}
285 +
286 +#define WSLC_API_MIN_VERSION_SUPPORTED 2, 8, 0
287 +
288 +bool DoesWslRuntimeVersionSupportWslc(const std::optional<std::tuple<uint32_t, uint32_t, uint32_t>>& version)
289 +{
290 + constexpr auto minimalPackageVersion = std::tuple<uint32_t, uint32_t, uint32_t>{WSLC_API_MIN_VERSION_SUPPORTED};
291 + return version.has_value() && version >= minimalPackageVersion;
292 +}
293 +
294 +enum class WslRuntimeState
295 +{
296 + NotInstalled,
297 + InstalledWithoutWslcSupport,
298 + InstalledWithWslcSupport,
299 +};
300 +
301 +WslRuntimeState CheckWslRuntimeState()
302 +{
303 + auto version = wsl::windows::common::wslutil::GetInstalledPackageVersion();
304 +
305 + if (!version.has_value())
306 + {
307 + return WslRuntimeState::NotInstalled;
308 + }
309 +
310 + return DoesWslRuntimeVersionSupportWslc(version) ? WslRuntimeState::InstalledWithWslcSupport : WslRuntimeState::InstalledWithoutWslcSupport;
311 +}
312 +
313 +std::pair<wil::com_ptr<IWSLCSessionManager>, HRESULT> CreateSessionManagerRaw()
314 +{
315 + wil::com_ptr<IWSLCSessionManager> result;
316 + HRESULT hr = CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&result));
317 + return {result, hr};
318 +}
319 +
320 +wil::com_ptr<IWSLCSessionManager> CreateSessionManager()
321 +{
322 + auto [result, hr] = CreateSessionManagerRaw();
323 +
324 + if (hr == REGDB_E_CLASSNOTREG)
325 + {
326 + WslRuntimeState currentState = CheckWslRuntimeState();
327 + THROW_WIN32_IF_MSG(
328 + ERROR_NOT_SUPPORTED,
329 + currentState == WslRuntimeState::InstalledWithoutWslcSupport,
330 + "The currently installed WSL version does not support WSLC.");
331 + THROW_HR_IF_MSG(
332 + hr,
333 + currentState == WslRuntimeState::InstalledWithWslcSupport,
334 + "The WSL install appears to be corrupted; session manager class was not registered.");
335 + }
336 +
337 + THROW_IF_FAILED(hr);
338 +
339 + wsl::windows::common::security::ConfigureForCOMImpersonation(result.get());
340 +
341 + return result;
342 +}
343 +
344 +bool NeedsWslRuntimeInstalled()
345 +{
346 + auto hr = CreateSessionManagerRaw().second;
347 +
348 + if (SUCCEEDED(hr))
349 + {
350 + return false;
351 + }
352 + else if (hr == REGDB_E_CLASSNOTREG)
353 + {
354 + return true;
355 + }
356 + THROW_HR(hr);
357 +}
358 +} // namespace
359 +
360 +// SESSION DEFINITIONS
361 +STDAPI WslcInitSessionSettings(_In_ PCWSTR name, _In_ PCWSTR storagePath, _Out_ WslcSessionSettings* sessionSettings)
362 +try
363 +{
364 + RETURN_HR_IF_NULL(E_POINTER, name);
365 + RETURN_HR_IF_NULL(E_POINTER, storagePath);
366 +
367 + auto internalType = CheckAndGetInternalType(sessionSettings);
368 +
369 + *internalType = {};
370 +
371 + internalType->displayName = name;
372 + internalType->storagePath = storagePath;
373 + internalType->cpuCount = s_DefaultCPUCount;
374 + internalType->memoryMb = s_DefaultMemoryMB;
375 + internalType->timeoutMS = s_DefaultBootTimeout;
376 + internalType->vhdRequirements.sizeBytes = s_DefaultStorageSize;
377 +
378 + return S_OK;
379 +}
380 +CATCH_RETURN();
381 +
382 +STDAPI WslcSetSessionSettingsCpuCount(_In_ WslcSessionSettings* sessionSettings, _In_ uint32_t cpuCount)
383 +try
384 +{
385 + auto internalType = CheckAndGetInternalType(sessionSettings);
386 +
387 + if (cpuCount)
388 + {
389 + internalType->cpuCount = cpuCount;
390 + }
391 + else
392 + {
393 + internalType->cpuCount = s_DefaultCPUCount;
394 + }
395 +
396 + return S_OK;
397 +}
398 +CATCH_RETURN();
399 +
400 +STDAPI WslcSetSessionSettingsMemory(_In_ WslcSessionSettings* sessionSettings, _In_ uint32_t memoryMB)
401 +try
402 +{
403 + auto internalType = CheckAndGetInternalType(sessionSettings);
404 +
405 + if (memoryMB)
406 + {
407 + internalType->memoryMb = memoryMB;
408 + }
409 + else
410 + {
411 + internalType->memoryMb = s_DefaultMemoryMB;
412 + }
413 +
414 + return S_OK;
415 +}
416 +CATCH_RETURN();
417 +
418 +STDAPI WslcCreateSession(_In_ WslcSessionSettings* sessionSettings, _Out_ WslcSession* session, _Outptr_opt_result_z_ PWSTR* errorMessage)
419 +try
420 +{
421 + RETURN_HR_IF_NULL(E_POINTER, session);
422 + *session = nullptr;
423 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
424 + auto internalType = CheckAndGetInternalType(sessionSettings);
425 +
426 + wil::com_ptr<IWSLCSessionManager> sessionManager = CreateSessionManager();
427 +
428 + auto result = std::make_unique<WslcSessionImpl>();
429 + WSLCSessionSettings runtimeSettings{};
430 + runtimeSettings.DisplayName = internalType->displayName;
431 + runtimeSettings.StoragePath = internalType->storagePath;
432 + runtimeSettings.MaximumStorageSizeMb = internalType->vhdRequirements.sizeBytes / _1MB;
433 + runtimeSettings.CpuCount = internalType->cpuCount;
434 + runtimeSettings.MemoryMb = internalType->memoryMb;
435 + runtimeSettings.BootTimeoutMs = internalType->timeoutMS;
436 + runtimeSettings.NetworkingMode = WSLCNetworkingModeVirtioProxy;
437 + auto terminationCallback = TerminationCallback::CreateIf(internalType);
438 + if (terminationCallback)
439 + {
440 + result->terminationCallback.attach(terminationCallback.as<ITerminationCallback>().detach());
441 + runtimeSettings.TerminationCallback = terminationCallback.get();
442 + }
443 + runtimeSettings.FeatureFlags = ConvertFlags(internalType->featureFlags);
444 + WI_SetFlag(runtimeSettings.FeatureFlags, WslcFeatureFlagsVirtioFs);
445 +
446 + if (SUCCEEDED(errorInfoWrapper.CaptureResult(sessionManager->CreateSession(&runtimeSettings, WSLCSessionFlagsNone, &result->session))))
447 + {
448 + wsl::windows::common::security::ConfigureForCOMImpersonation(result->session.get());
449 + *session = reinterpret_cast<WslcSession>(result.release());
450 + }
451 +
452 + return errorInfoWrapper;
453 +}
454 +CATCH_RETURN();
455 +
456 +STDAPI WslcTerminateSession(_In_ WslcSession session)
457 +try
458 +{
459 + auto internalType = CheckAndGetInternalType(session);
460 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
461 +
462 + RETURN_HR(internalType->session->Terminate());
463 +}
464 +CATCH_RETURN();
465 +
466 +STDAPI WslcSetSessionSettingsTimeout(_In_ WslcSessionSettings* sessionSettings, _In_ uint32_t timeoutMS)
467 +try
468 +{
469 + auto internalType = CheckAndGetInternalType(sessionSettings);
470 +
471 + if (timeoutMS)
472 + {
473 + internalType->timeoutMS = timeoutMS;
474 + }
475 + else
476 + {
477 + internalType->timeoutMS = s_DefaultBootTimeout;
478 + }
479 +
480 + return S_OK;
481 +}
482 +CATCH_RETURN();
483 +
484 +STDAPI WslcCreateSessionVhdVolume(_In_ WslcSession session, _In_ const WslcVhdRequirements* options, _Outptr_opt_result_z_ PWSTR* errorMessage)
485 +try
486 +{
487 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
488 +
489 + auto internalType = CheckAndGetInternalType(session);
490 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
491 + RETURN_HR_IF_NULL(E_POINTER, options);
492 +
493 + RETURN_HR_IF_NULL(E_INVALIDARG, options->name);
494 + RETURN_HR_IF(E_INVALIDARG, options->sizeBytes == 0);
495 + RETURN_HR_IF(E_NOTIMPL, options->type != WSLC_VHD_TYPE_DYNAMIC);
496 +
497 + WSLCVolumeOptions volumeOptions{};
498 + volumeOptions.Name = options->name;
499 + volumeOptions.Driver = "vhd";
500 +
501 + auto sizeStr = std::to_string(options->sizeBytes);
502 + WSLCDriverOption driverOpts[] = {{"SizeBytes", sizeStr.c_str()}};
503 + volumeOptions.DriverOpts = driverOpts;
504 + volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
505 +
506 + WSLCVolumeInformation volumeInfo{};
507 + return errorInfoWrapper.CaptureResult(internalType->session->CreateVolume(&volumeOptions, &volumeInfo));
508 +}
509 +CATCH_RETURN();
510 +
511 +STDAPI WslcDeleteSessionVhdVolume(_In_ WslcSession session, _In_z_ PCSTR name, _Outptr_opt_result_z_ PWSTR* errorMessage)
512 +try
513 +{
514 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
515 +
516 + auto internalType = CheckAndGetInternalType(session);
517 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
518 + RETURN_HR_IF_NULL(E_POINTER, name);
519 +
520 + return errorInfoWrapper.CaptureResult(internalType->session->DeleteVolume(name));
521 +}
522 +CATCH_RETURN();
523 +
524 +STDAPI WslcSetSessionSettingsVhd(_In_ WslcSessionSettings* sessionSettings, _In_opt_ const WslcVhdRequirements* vhdRequirements)
525 +try
526 +{
527 + auto internalType = CheckAndGetInternalType(sessionSettings);
528 +
529 + if (vhdRequirements)
530 + {
531 + RETURN_HR_IF(E_INVALIDARG, vhdRequirements->sizeBytes == 0);
532 + RETURN_HR_IF(E_NOTIMPL, vhdRequirements->type != WSLC_VHD_TYPE_DYNAMIC);
533 +
534 + internalType->vhdRequirements = *vhdRequirements;
535 + }
536 + else
537 + {
538 + internalType->vhdRequirements = {};
539 + internalType->vhdRequirements.sizeBytes = s_DefaultStorageSize;
540 + }
541 +
542 + return S_OK;
543 +}
544 +CATCH_RETURN();
545 +
546 +STDAPI WslcSetSessionSettingsFeatureFlags(_In_ WslcSessionSettings* sessionSettings, _In_ WslcSessionFeatureFlags flags)
547 +try
548 +{
549 + auto internalType = CheckAndGetInternalType(sessionSettings);
550 +
551 + internalType->featureFlags = flags;
552 +
553 + return S_OK;
554 +}
555 +CATCH_RETURN();
556 +
557 +STDAPI WslcSetSessionSettingsTerminationCallback(
558 + _In_ WslcSessionSettings* sessionSettings, _In_opt_ WslcSessionTerminationCallback terminationCallback, _In_opt_ PVOID terminationContext)
559 +try
560 +{
561 + auto internalType = CheckAndGetInternalType(sessionSettings);
562 + RETURN_HR_IF(E_INVALIDARG, terminationCallback == nullptr && terminationContext != nullptr);
563 +
564 + internalType->terminationCallback = terminationCallback;
565 + internalType->terminationCallbackContext = terminationContext;
566 +
567 + return S_OK;
568 +}
569 +CATCH_RETURN();
570 +
571 +STDAPI WslcReleaseSession(_In_ WslcSession session)
572 +try
573 +{
574 + auto internalType = CheckAndGetInternalTypeUniquePointer(session);
575 +
576 + // Intentionally destroy session before termination callback in the event that
577 + // the termination callback ends up being invoked by session destruction.
578 + internalType->session.reset();
579 + internalType->terminationCallback.reset();
580 +
581 + return S_OK;
582 +}
583 +CATCH_RETURN();
584 +
585 +STDAPI WslcReleaseContainer(_In_ WslcContainer container)
586 +try
587 +{
588 + CheckAndGetInternalTypeUniquePointer(container);
589 +
590 + return S_OK;
591 +}
592 +CATCH_RETURN();
593 +
594 +STDAPI WslcReleaseProcess(_In_ WslcProcess process)
595 +try
596 +{
597 + CheckAndGetInternalTypeUniquePointer(process);
598 +
599 + return S_OK;
600 +}
601 +CATCH_RETURN();
602 +
603 +// CONTAINER DEFINITIONS
604 +
605 +STDAPI WslcInitContainerSettings(_In_ PCSTR imageName, _Out_ WslcContainerSettings* containerSettings)
606 +try
607 +{
608 + auto internalType = CheckAndGetInternalType(containerSettings);
609 + RETURN_HR_IF_NULL(E_POINTER, imageName);
610 +
611 + *internalType = {};
612 +
613 + internalType->image = imageName;
614 + // Default network configuration to WSLC SDK `0`, which is NONE.
615 + internalType->networking = WSLCContainerNetworkTypeNone;
616 +
617 + return S_OK;
618 +}
619 +CATCH_RETURN();
620 +
621 +STDAPI WslcCreateContainer(_In_ WslcSession session, _In_ const WslcContainerSettings* containerSettings, _Out_ WslcContainer* container, _Outptr_opt_result_z_ PWSTR* errorMessage)
622 +try
623 +{
624 + RETURN_HR_IF_NULL(E_POINTER, container);
625 + *container = nullptr;
626 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
627 + auto internalSession = CheckAndGetInternalType(session);
628 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalSession->session);
629 + auto internalContainerSettings = CheckAndGetInternalType(containerSettings);
630 +
631 + auto result = std::make_unique<WslcContainerImpl>();
632 +
633 + WSLCContainerOptions containerOptions{};
634 + std::unique_ptr<WSLCPortMapping[]> convertedPorts; // this must stay in same scope as containerOptions since containerOptions.Ports is getting a raw pointer to the array owned by convertedPorts.
635 +
636 + containerOptions.Image = internalContainerSettings->image;
637 + containerOptions.Name = internalContainerSettings->runtimeName;
638 + containerOptions.HostName = internalContainerSettings->HostName;
639 + containerOptions.DomainName = internalContainerSettings->DomainName;
640 + containerOptions.Flags = ConvertFlags(internalContainerSettings->containerFlags);
641 +
642 + CopyProcessSettingsToRuntime(containerOptions.InitProcessOptions, internalContainerSettings->initProcessOptions);
643 +
644 + std::unique_ptr<WSLCVolume[]> convertedVolumes;
645 + if (internalContainerSettings->volumes && internalContainerSettings->volumesCount)
646 + {
647 + convertedVolumes = std::make_unique<WSLCVolume[]>(internalContainerSettings->volumesCount);
648 + for (uint32_t i = 0; i < internalContainerSettings->volumesCount; ++i)
649 + {
650 + const WslcContainerVolume& internalVolume = internalContainerSettings->volumes[i];
651 + WSLCVolume& convertedVolume = convertedVolumes[i];
652 +
653 + convertedVolume.HostPath = internalVolume.windowsPath;
654 + convertedVolume.ContainerPath = internalVolume.containerPath;
655 + convertedVolume.ReadOnly = internalVolume.readOnly;
656 + }
657 + containerOptions.Volumes = convertedVolumes.get();
658 + containerOptions.VolumesCount = static_cast<ULONG>(internalContainerSettings->volumesCount);
659 + }
660 +
661 + std::unique_ptr<WSLCNamedVolume[]> convertedNamedVolumes;
662 + if (internalContainerSettings->namedVolumes && internalContainerSettings->namedVolumesCount)
663 + {
664 + convertedNamedVolumes = std::make_unique<WSLCNamedVolume[]>(internalContainerSettings->namedVolumesCount);
665 + for (uint32_t i = 0; i < internalContainerSettings->namedVolumesCount; ++i)
666 + {
667 + const WslcContainerNamedVolume& internalVolume = internalContainerSettings->namedVolumes[i];
668 + WSLCNamedVolume& convertedVolume = convertedNamedVolumes[i];
669 +
670 + convertedVolume.Name = internalVolume.name;
671 + convertedVolume.ContainerPath = internalVolume.containerPath;
672 + convertedVolume.ReadOnly = internalVolume.readOnly;
673 + }
674 + containerOptions.NamedVolumes = convertedNamedVolumes.get();
675 + containerOptions.NamedVolumesCount = static_cast<ULONG>(internalContainerSettings->namedVolumesCount);
676 + }
677 +
678 + if (internalContainerSettings->ports && internalContainerSettings->portsCount)
679 + {
680 + convertedPorts = std::make_unique<WSLCPortMapping[]>(internalContainerSettings->portsCount);
681 + for (uint32_t i = 0; i < internalContainerSettings->portsCount; ++i)
682 + {
683 + const WslcContainerPortMapping& internalPort = internalContainerSettings->ports[i];
684 + WSLCPortMapping& convertedPort = convertedPorts[i];
685 +
686 + convertedPort.HostPort = internalPort.windowsPort;
687 + convertedPort.ContainerPort = internalPort.containerPort;
688 +
689 + // TODO: Consider using standard protocol numbers instead of our own enum.
690 + switch (internalPort.protocol)
691 + {
692 + case WSLC_PORT_PROTOCOL_TCP:
693 + convertedPort.Protocol = IPPROTO_TCP;
694 + break;
695 + case WSLC_PORT_PROTOCOL_UDP:
696 + convertedPort.Protocol = IPPROTO_UDP;
697 + break;
698 + default:
699 + THROW_HR_MSG(E_INVALIDARG, "Unsupported port protocol: %u", internalPort.protocol);
700 + }
701 + // Validate IP address if provided and if valid, copy to runtime structure.
702 + if (internalPort.windowsAddress != nullptr)
703 + {
704 + switch (internalPort.windowsAddress->ss_family)
705 + {
706 + case AF_INET:
707 + {
708 + const auto* addr4 = reinterpret_cast<const sockaddr_in*>(internalPort.windowsAddress);
709 + HRESULT hr = InetNtopToHresult(AF_INET, &addr4->sin_addr, convertedPort.BindingAddress, sizeof(convertedPort.BindingAddress));
710 + if (FAILED(hr))
711 + {
712 + THROW_HR_MSG(hr, "inet_ntop() failed for AF_INET address");
713 + }
714 + convertedPort.Family = AF_INET;
715 + break;
716 + }
717 +
718 + case AF_INET6:
719 + {
720 + const auto* addr6 = reinterpret_cast<const sockaddr_in6*>(internalPort.windowsAddress);
721 + HRESULT hr = InetNtopToHresult(AF_INET6, &addr6->sin6_addr, convertedPort.BindingAddress, sizeof(convertedPort.BindingAddress));
722 + if (FAILED(hr))
723 + {
724 + THROW_HR_MSG(hr, "inet_ntop() failed for AF_INET6 address");
725 + }
726 + convertedPort.Family = AF_INET6;
727 + break;
728 + }
729 +
730 + default:
731 + THROW_HR_MSG(E_INVALIDARG, "Unsupported address family: %d", internalPort.windowsAddress->ss_family);
732 + }
733 + }
734 + else
735 + {
736 + convertedPort.Family = AF_INET;
737 + strcpy_s(convertedPort.BindingAddress, "127.0.0.1");
738 + }
739 + }
740 + containerOptions.Ports = convertedPorts.get(); // Make sure convertedPorts stays in scope for life of containerOptions
741 + containerOptions.PortsCount = static_cast<ULONG>(internalContainerSettings->portsCount);
742 + }
743 +
744 + containerOptions.ContainerNetwork.ContainerNetworkType = internalContainerSettings->networking;
745 +
746 + // TODO: No user access
747 + // containerOptions.Labels;
748 + // containerOptions.LabelsCount;
749 + // containerOptions.StopSignal;
750 + // containerOptions.ShmSize;
751 +
752 + if (SUCCEEDED(errorInfoWrapper.CaptureResult(internalSession->session->CreateContainer(&containerOptions, &result->container))))
753 + {
754 + wsl::windows::common::security::ConfigureForCOMImpersonation(result->container.get());
755 +
756 + if (IOCallback::HasIOCallback(internalContainerSettings->initProcessOptions))
757 + {
758 + result->ioCallbackOptions = internalContainerSettings->initProcessOptions->ioCallbacks;
759 + }
760 +
761 + *container = reinterpret_cast<WslcContainer>(result.release());
762 + }
763 +
764 + return errorInfoWrapper;
765 +}
766 +CATCH_RETURN();
767 +
768 +STDAPI WslcStartContainer(_In_ WslcContainer container, _In_ WslcContainerStartFlags flags, _Outptr_opt_result_z_ PWSTR* errorMessage)
769 +try
770 +{
771 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
772 + auto internalType = CheckAndGetInternalType(container);
773 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->container);
774 +
775 + bool hasIOCallback = IOCallback::HasIOCallback(internalType->ioCallbackOptions);
776 + // If callbacks were provided, ATTACH must be used.
777 + // TODO: Consider if we should just override flags when callbacks were provided instead.
778 + RETURN_HR_IF(E_INVALIDARG, WI_IsFlagClear(flags, WSLC_CONTAINER_START_FLAG_ATTACH) && hasIOCallback);
779 +
780 + if (SUCCEEDED(errorInfoWrapper.CaptureResult(internalType->container->Start(ConvertFlags(flags), nullptr))))
781 + {
782 + if (hasIOCallback)
783 + {
784 + wil::com_ptr<IWSLCProcess> process;
785 + RETURN_IF_FAILED(internalType->container->GetInitProcess(&process));
786 + wsl::windows::common::security::ConfigureForCOMImpersonation(process.get());
787 + internalType->ioCallbacks = std::make_shared<IOCallback>(process.get(), internalType->ioCallbackOptions);
788 + }
789 + }
790 +
791 + return errorInfoWrapper;
792 +}
793 +CATCH_RETURN();
794 +
795 +STDAPI WslcSetContainerSettingsFlags(_In_ WslcContainerSettings* containerSettings, _In_ WslcContainerFlags flags)
796 +try
797 +{
798 + auto internalType = CheckAndGetInternalType(containerSettings);
799 +
800 + internalType->containerFlags = flags;
801 +
802 + return S_OK;
803 +}
804 +CATCH_RETURN();
805 +
806 +STDAPI WslcSetContainerSettingsName(_In_ WslcContainerSettings* containerSettings, _In_ PCSTR name)
807 +try
808 +{
809 + auto internalType = CheckAndGetInternalType(containerSettings);
810 +
811 + internalType->runtimeName = name;
812 +
813 + return S_OK;
814 +}
815 +CATCH_RETURN();
816 +
817 +STDAPI WslcSetContainerSettingsHostName(_In_ WslcContainerSettings* containerSettings, _In_ PCSTR hostName)
818 +try
819 +{
820 + auto internalType = CheckAndGetInternalType(containerSettings);
821 +
822 + internalType->HostName = hostName;
823 +
824 + return S_OK;
825 +}
826 +CATCH_RETURN();
827 +
828 +STDAPI WslcSetContainerSettingsDomainName(_In_ WslcContainerSettings* containerSettings, _In_ PCSTR domainName)
829 +try
830 +{
831 + auto internalType = CheckAndGetInternalType(containerSettings);
832 +
833 + internalType->DomainName = domainName;
834 +
835 + return S_OK;
836 +}
837 +CATCH_RETURN();
838 +
839 +STDAPI WslcSetContainerSettingsInitProcess(_In_ WslcContainerSettings* containerSettings, _In_ WslcProcessSettings* initProcess)
840 +try
841 +{
842 + auto internalType = CheckAndGetInternalType(containerSettings);
843 +
844 + internalType->initProcessOptions = GetInternalType(initProcess);
845 +
846 + return S_OK;
847 +}
848 +CATCH_RETURN();
849 +
850 +STDAPI WslcSetContainerSettingsNetworkingMode(_In_ WslcContainerSettings* containerSettings, _In_ WslcContainerNetworkingMode networkingMode)
851 +try
852 +{
853 + auto internalType = CheckAndGetInternalType(containerSettings);
854 +
855 + internalType->networking = Convert(networkingMode);
856 +
857 + return S_OK;
858 +}
859 +CATCH_RETURN();
860 +
861 +STDAPI WslcSetContainerSettingsPortMappings(
862 + _In_ WslcContainerSettings* containerSettings, _In_reads_opt_(portMappingCount) const WslcContainerPortMapping* portMappings, _In_ uint32_t portMappingCount)
863 +try
864 +{
865 + auto internalType = CheckAndGetInternalType(containerSettings);
866 + RETURN_HR_IF(E_INVALIDARG, (portMappings == nullptr && portMappingCount != 0) || (portMappings != nullptr && portMappingCount == 0));
867 +
868 + for (uint32_t i = 0; i < portMappingCount; ++i)
869 + {
870 + if (portMappings[i].windowsAddress != nullptr)
871 + {
872 + const auto family = portMappings[i].windowsAddress->ss_family;
873 + RETURN_HR_IF_MSG(
874 + E_INVALIDARG, family != AF_INET && family != AF_INET6, "Unsupported address family: %d at port mapping index %u", family, i);
875 + }
876 + RETURN_HR_IF_MSG(
877 + E_NOTIMPL, portMappings[i].protocol != 0, "Unsupported protocol: %d at port mapping index %u", portMappings[i].protocol, i);
878 + }
879 + internalType->ports = portMappings;
880 + internalType->portsCount = portMappingCount;
881 +
882 + return S_OK;
883 +}
884 +CATCH_RETURN();
885 +
886 +STDAPI WslcSetContainerSettingsVolumes(
887 + _In_ WslcContainerSettings* containerSettings, _In_reads_opt_(volumeCount) const WslcContainerVolume* volumes, _In_ uint32_t volumeCount)
888 +try
889 +{
890 + auto internalType = CheckAndGetInternalType(containerSettings);
891 + RETURN_HR_IF(E_INVALIDARG, (volumes == nullptr && volumeCount != 0) || (volumes != nullptr && volumeCount == 0));
892 +
893 + for (uint32_t i = 0; i < volumeCount; ++i)
894 + {
895 + RETURN_HR_IF_NULL(E_INVALIDARG, volumes[i].windowsPath);
896 + EnsureAbsolutePath(volumes[i].windowsPath, false);
897 + RETURN_HR_IF_NULL(E_INVALIDARG, volumes[i].containerPath);
898 + EnsureAbsolutePath(volumes[i].containerPath, true);
899 + }
900 +
901 + internalType->volumes = volumes;
902 + internalType->volumesCount = volumeCount;
903 +
904 + return S_OK;
905 +}
906 +CATCH_RETURN();
907 +
908 +STDAPI WslcSetContainerSettingsNamedVolumes(
909 + _In_ WslcContainerSettings* containerSettings, _In_reads_opt_(namedVolumeCount) const WslcContainerNamedVolume* namedVolumes, _In_ uint32_t namedVolumeCount)
910 +try
911 +{
912 + auto internalType = CheckAndGetInternalType(containerSettings);
913 + RETURN_HR_IF(E_INVALIDARG, (namedVolumes == nullptr && namedVolumeCount != 0) || (namedVolumes != nullptr && namedVolumeCount == 0));
914 +
915 + for (uint32_t i = 0; i < namedVolumeCount; ++i)
916 + {
917 + RETURN_HR_IF_NULL(E_INVALIDARG, namedVolumes[i].name);
918 + RETURN_HR_IF_NULL(E_INVALIDARG, namedVolumes[i].containerPath);
919 + EnsureAbsolutePath(namedVolumes[i].containerPath, true);
920 + }
921 +
922 + internalType->namedVolumes = namedVolumes;
923 + internalType->namedVolumesCount = namedVolumeCount;
924 +
925 + return S_OK;
926 +}
927 +CATCH_RETURN();
928 +
929 +STDAPI WslcCreateContainerProcess(
930 + _In_ WslcContainer container, _In_ WslcProcessSettings* newProcessSettings, _Out_ WslcProcess* newProcess, _Outptr_opt_result_z_ PWSTR* errorMessage)
931 +try
932 +{
933 + RETURN_HR_IF_NULL(E_POINTER, newProcess);
934 + *newProcess = nullptr;
935 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
936 + auto internalContainer = CheckAndGetInternalType(container);
937 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalContainer->container);
938 + auto internalProcessSettings = CheckAndGetInternalType(newProcessSettings);
939 + RETURN_HR_IF(E_INVALIDARG, internalProcessSettings->commandLine == nullptr || internalProcessSettings->commandLineCount == 0);
940 +
941 + WSLCProcessOptions runtimeOptions{};
942 + CopyProcessSettingsToRuntime(runtimeOptions, internalProcessSettings);
943 +
944 + auto result = std::make_unique<WslcProcessImpl>();
945 + if (SUCCEEDED(errorInfoWrapper.CaptureResult(internalContainer->container->Exec(&runtimeOptions, nullptr, &result->process))))
946 + {
947 + wsl::windows::common::security::ConfigureForCOMImpersonation(result->process.get());
948 +
949 + if (IOCallback::HasIOCallback(internalProcessSettings))
950 + {
951 + result->ioCallbacks = std::make_shared<IOCallback>(result->process.get(), internalProcessSettings->ioCallbacks);
952 + }
953 +
954 + *newProcess = reinterpret_cast<WslcProcess>(result.release());
955 + }
956 +
957 + return errorInfoWrapper;
958 +}
959 +CATCH_RETURN();
960 +
961 +// GENERAL CONTAINER MANAGEMENT
962 +
963 +STDAPI WslcGetContainerID(WslcContainer container, CHAR containerID[WSLC_CONTAINER_ID_BUFFER_SIZE])
964 +try
965 +{
966 + static_assert(WSLC_CONTAINER_ID_BUFFER_SIZE == sizeof(WSLCContainerId), "Container ID lengths differ.");
967 +
968 + auto internalType = CheckAndGetInternalType(container);
969 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->container);
970 + RETURN_HR_IF_NULL(E_POINTER, containerID);
971 +
972 + return internalType->container->GetId(containerID);
973 +}
974 +CATCH_RETURN();
975 +
976 +STDAPI WslcInspectContainer(_In_ WslcContainer container, _Outptr_result_z_ PSTR* inspectData)
977 +try
978 +{
979 + auto internalType = CheckAndGetInternalType(container);
980 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->container);
981 + RETURN_HR_IF_NULL(E_POINTER, inspectData);
982 +
983 + *inspectData = nullptr;
984 +
985 + wil::unique_cotaskmem_ansistring result;
986 + RETURN_IF_FAILED(internalType->container->Inspect(&result));
987 +
988 + *inspectData = result.release();
989 +
990 + return S_OK;
991 +}
992 +CATCH_RETURN();
993 +
994 +STDAPI WslcGetContainerInitProcess(_In_ WslcContainer container, _Out_ WslcProcess* initProcess)
995 +try
996 +{
997 + RETURN_HR_IF_NULL(E_POINTER, initProcess);
998 + *initProcess = nullptr;
999 + auto internalType = CheckAndGetInternalType(container);
1000 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->container);
1001 +
1002 + auto result = std::make_unique<WslcProcessImpl>();
1003 +
1004 + RETURN_IF_FAILED(internalType->container->GetInitProcess(&result->process));
1005 +
1006 + wsl::windows::common::security::ConfigureForCOMImpersonation(result->process.get());
1007 +
1008 + result->ioCallbacks = internalType->ioCallbacks.load();
1009 +
1010 + *initProcess = reinterpret_cast<WslcProcess>(result.release());
1011 +
1012 + return S_OK;
1013 +}
1014 +CATCH_RETURN();
1015 +
1016 +STDAPI WslcGetContainerState(_In_ WslcContainer container, _Out_ WslcContainerState* state)
1017 +try
1018 +{
1019 + static_assert(
1020 + WSLC_CONTAINER_STATE_INVALID == WslcContainerStateInvalid && WSLC_CONTAINER_STATE_CREATED == WslcContainerStateCreated &&
1021 + WSLC_CONTAINER_STATE_RUNNING == WslcContainerStateRunning &&
1022 + WSLC_CONTAINER_STATE_EXITED == WslcContainerStateExited && WSLC_CONTAINER_STATE_DELETED == WslcContainerStateDeleted,
1023 + "Container state enum values mismatch.");
1024 +
1025 + auto internalType = CheckAndGetInternalType(container);
1026 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->container);
1027 + RETURN_HR_IF_NULL(E_POINTER, state);
1028 +
1029 + *state = WSLC_CONTAINER_STATE_INVALID;
1030 +
1031 + WSLCContainerState runtimeState{};
1032 + RETURN_IF_FAILED(internalType->container->GetState(&runtimeState));
1033 +
1034 + *state = static_cast<WslcContainerState>(runtimeState);
1035 + return S_OK;
1036 +}
1037 +CATCH_RETURN();
1038 +
1039 +STDAPI WslcStopContainer(_In_ WslcContainer container, _In_ WslcSignal signal, _In_ uint32_t timeoutSeconds, _Outptr_opt_result_z_ PWSTR* errorMessage)
1040 +try
1041 +{
1042 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1043 + auto internalType = CheckAndGetInternalType(container);
1044 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->container);
1045 +
1046 + return errorInfoWrapper.CaptureResult(internalType->container->Stop(Convert(signal), timeoutSeconds));
1047 +}
1048 +CATCH_RETURN();
1049 +
1050 +STDAPI WslcDeleteContainer(_In_ WslcContainer container, _In_ WslcDeleteContainerFlags flags, _Outptr_opt_result_z_ PWSTR* errorMessage)
1051 +try
1052 +{
1053 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1054 + auto internalType = CheckAndGetInternalType(container);
1055 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->container);
1056 +
1057 + return errorInfoWrapper.CaptureResult(internalType->container->Delete(ConvertFlags(flags)));
1058 +}
1059 +CATCH_RETURN();
1060 +
1061 +// PROCESS DEFINITIONS
1062 +
1063 +STDAPI WslcInitProcessSettings(_Out_ WslcProcessSettings* processSettings)
1064 +try
1065 +{
1066 + auto internalType = CheckAndGetInternalType(processSettings);
1067 +
1068 + *internalType = {};
1069 +
1070 + return S_OK;
1071 +}
1072 +CATCH_RETURN();
1073 +
1074 +STDAPI WslcSetProcessSettingsWorkingDirectory(_In_ WslcProcessSettings* processSettings, _In_ PCSTR workingDirectory)
1075 +try
1076 +{
1077 + auto internalType = CheckAndGetInternalType(processSettings);
1078 +
1079 + internalType->workingDirectory = workingDirectory;
1080 +
1081 + return S_OK;
1082 +}
1083 +CATCH_RETURN();
1084 +
1085 +// OPTIONAL PROCESS SETTINGS
1086 +
1087 +STDAPI WslcSetProcessSettingsCmdLine(_In_ WslcProcessSettings* processSettings, _In_reads_(argc) PCSTR const* argv, size_t argc)
1088 +try
1089 +{
1090 + auto internalType = CheckAndGetInternalType(processSettings);
1091 + RETURN_HR_IF(
1092 + E_INVALIDARG,
1093 + (argv == nullptr && argc != 0) || (argv != nullptr && argc == 0) ||
1094 + (argc > static_cast<size_t>(std::numeric_limits<uint32_t>::max())));
1095 +
1096 + internalType->commandLine = argv;
1097 + internalType->commandLineCount = static_cast<uint32_t>(argc);
1098 +
1099 + return S_OK;
1100 +}
1101 +CATCH_RETURN();
1102 +
1103 +STDAPI WslcSetProcessSettingsEnvVariables(_In_ WslcProcessSettings* processSettings, _In_reads_(argc) PCSTR const* key_value, size_t argc)
1104 +try
1105 +{
1106 + auto internalType = CheckAndGetInternalType(processSettings);
1107 + RETURN_HR_IF(
1108 + E_INVALIDARG,
1109 + (key_value == nullptr && argc != 0) || (key_value != nullptr && argc == 0) ||
1110 + (argc > static_cast<size_t>(std::numeric_limits<uint32_t>::max())));
1111 +
1112 + internalType->environment = key_value;
1113 + internalType->environmentCount = static_cast<uint32_t>(argc);
1114 +
1115 + return S_OK;
1116 +}
1117 +CATCH_RETURN();
1118 +
1119 +// PROCESS MANAGEMENT
1120 +
1121 +STDAPI WslcGetProcessPid(_In_ WslcProcess process, _Out_ uint32_t* pid)
1122 +try
1123 +{
1124 + auto internalType = CheckAndGetInternalType(process);
1125 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->process);
1126 + RETURN_HR_IF_NULL(E_POINTER, pid);
1127 +
1128 + *pid = 0;
1129 +
1130 + int runtimePid{};
1131 + RETURN_IF_FAILED(internalType->process->GetPid(&runtimePid));
1132 +
1133 + *pid = static_cast<uint32_t>(runtimePid);
1134 + return S_OK;
1135 +}
1136 +CATCH_RETURN();
1137 +
1138 +STDAPI WslcGetProcessExitEvent(_In_ WslcProcess process, _Out_ HANDLE* exitEvent)
1139 +try
1140 +{
1141 + auto internalType = CheckAndGetInternalType(process);
1142 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->process);
1143 + RETURN_HR_IF_NULL(E_POINTER, exitEvent);
1144 +
1145 + return internalType->process->GetExitEvent(exitEvent);
1146 +}
1147 +CATCH_RETURN();
1148 +
1149 +// PROCESS RESULT / SIGNALS
1150 +
1151 +STDAPI WslcGetProcessState(_In_ WslcProcess process, _Out_ WslcProcessState* state)
1152 +try
1153 +{
1154 + static_assert(
1155 + WSLC_PROCESS_STATE_UNKNOWN == WslcProcessStateUnknown && WSLC_PROCESS_STATE_RUNNING == WslcProcessStateRunning &&
1156 + WSLC_PROCESS_STATE_EXITED == WslcProcessStateExited && WSLC_PROCESS_STATE_SIGNALLED == WslcProcessStateSignalled,
1157 + "Process state enum values mismatch.");
1158 +
1159 + auto internalType = CheckAndGetInternalType(process);
1160 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->process);
1161 + RETURN_HR_IF_NULL(E_POINTER, state);
1162 +
1163 + *state = WSLC_PROCESS_STATE_UNKNOWN;
1164 +
1165 + WSLCProcessState runtimeState{};
1166 + int exitCode{};
1167 + RETURN_IF_FAILED(internalType->process->GetState(&runtimeState, &exitCode));
1168 +
1169 + *state = static_cast<WslcProcessState>(runtimeState);
1170 + return S_OK;
1171 +}
1172 +CATCH_RETURN();
1173 +
1174 +STDAPI WslcGetProcessExitCode(_In_ WslcProcess process, _Out_ PINT32 exitCode)
1175 +try
1176 +{
1177 + auto internalType = CheckAndGetInternalType(process);
1178 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->process);
1179 + RETURN_HR_IF_NULL(E_POINTER, exitCode);
1180 +
1181 + *exitCode = -1;
1182 +
1183 + WSLCProcessState runtimeState{};
1184 + RETURN_IF_FAILED(internalType->process->GetState(&runtimeState, exitCode));
1185 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), runtimeState != WslcProcessStateExited);
1186 + return S_OK;
1187 +}
1188 +CATCH_RETURN();
1189 +
1190 +STDAPI WslcSignalProcess(_In_ WslcProcess process, _In_ WslcSignal signal)
1191 +try
1192 +{
1193 + auto internalType = CheckAndGetInternalType(process);
1194 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->process);
1195 +
1196 + RETURN_HR(internalType->process->Signal(Convert(signal)));
1197 +}
1198 +CATCH_RETURN();
1199 +
1200 +STDAPI WslcSetProcessSettingsCallbacks(_In_ WslcProcessSettings* processSettings, _In_ const WslcProcessCallbacks* callbacks, _In_opt_ PVOID context)
1201 +try
1202 +{
1203 + auto internalType = CheckAndGetInternalType(processSettings);
1204 + RETURN_HR_IF(E_INVALIDARG, callbacks == nullptr && context != nullptr);
1205 +
1206 + static_assert(std::is_trivial_v<WslcProcessCallbacks>, "WslcProcessCallbacks must be trivial.");
1207 +
1208 + WslcProcessCallbacks* internalCallbacks = &internalType->ioCallbacks;
1209 +
1210 + if (callbacks)
1211 + {
1212 + *internalCallbacks = *callbacks;
1213 + internalType->ioCallbacks.callbackContext = context;
1214 + }
1215 + else
1216 + {
1217 + *internalCallbacks = {};
1218 + }
1219 +
1220 + return S_OK;
1221 +}
1222 +CATCH_RETURN();
1223 +
1224 +STDAPI WslcGetProcessIOHandle(_In_ WslcProcess process, _In_ WslcProcessIOHandle ioHandle, _Out_ HANDLE* handle)
1225 +try
1226 +{
1227 + auto internalType = CheckAndGetInternalType(process);
1228 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->process);
1229 + RETURN_HR_IF_NULL(E_POINTER, handle);
1230 +
1231 + *handle = nullptr;
1232 +
1233 + auto result = IOCallback::GetIOHandle(internalType->process.get(), ioHandle);
1234 + *handle = result.release();
1235 +
1236 + return S_OK;
1237 +}
1238 +CATCH_RETURN();
1239 +
1240 +// IMAGE MANAGEMENT
1241 +STDAPI WslcPullSessionImage(_In_ WslcSession session, _In_ const WslcPullImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage)
1242 +try
1243 +{
1244 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1245 + auto internalType = CheckAndGetInternalType(session);
1246 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1247 + RETURN_HR_IF_NULL(E_POINTER, options);
1248 + RETURN_HR_IF_NULL(E_INVALIDARG, options->uri);
1249 +
1250 + auto progressCallback = ProgressCallback::CreateIf(options);
1251 +
1252 + return errorInfoWrapper.CaptureResult(internalType->session->PullImage(options->uri, options->registryAuth, progressCallback.get()));
1253 +}
1254 +CATCH_RETURN();
1255 +
1256 +static HRESULT WslcImportSessionImageImpl(
1257 + WslcSessionImpl* internalSession, PCSTR imageName, const WslcImportImageOptions* options, ErrorInfoWrapper& errorInfoWrapper, const ImageFileResolver& imageFile)
1258 +{
1259 + auto progressCallback = ProgressCallback::CreateIf(options);
1260 +
1261 + return errorInfoWrapper.CaptureResult(internalSession->session->ImportImage(
1262 + ToCOMInputHandle(imageFile.Handle()), imageName, progressCallback.get(), imageFile.Length()));
1263 +}
1264 +
1265 +STDAPI WslcImportSessionImage(
1266 + _In_ WslcSession session,
1267 + _In_z_ PCSTR imageName,
1268 + _In_ HANDLE imageContent,
1269 + _In_ uint64_t imageContentLength,
1270 + _In_opt_ const WslcImportImageOptions* options,
1271 + _Outptr_opt_result_z_ PWSTR* errorMessage)
1272 +try
1273 +{
1274 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1275 + auto internalType = CheckAndGetInternalType(session);
1276 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1277 + THROW_HR_IF_NULL(E_POINTER, imageName);
1278 + return WslcImportSessionImageImpl(internalType, imageName, options, errorInfoWrapper, {imageContent, imageContentLength});
1279 +}
1280 +CATCH_RETURN();
1281 +
1282 +STDAPI WslcImportSessionImageFromFile(
1283 + _In_ WslcSession session, _In_z_ PCSTR imageName, _In_z_ PCWSTR path, _In_opt_ const WslcImportImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage)
1284 +try
1285 +{
1286 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1287 + auto internalType = CheckAndGetInternalType(session);
1288 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1289 + THROW_HR_IF_NULL(E_POINTER, imageName);
1290 + return WslcImportSessionImageImpl(internalType, imageName, options, errorInfoWrapper, {path});
1291 +}
1292 +CATCH_RETURN();
1293 +
1294 +static HRESULT WslcLoadSessionImageImpl(
1295 + WslcSessionImpl* internalSession, const WslcLoadImageOptions* options, ErrorInfoWrapper& errorInfoWrapper, const ImageFileResolver& imageFile)
1296 +{
1297 + auto progressCallback = ProgressCallback::CreateIf(options);
1298 +
1299 + return errorInfoWrapper.CaptureResult(
1300 + internalSession->session->LoadImage(ToCOMInputHandle(imageFile.Handle()), progressCallback.get(), imageFile.Length()));
1301 +}
1302 +
1303 +STDAPI WslcLoadSessionImage(
1304 + _In_ WslcSession session,
1305 + _In_ HANDLE imageContent,
1306 + _In_ uint64_t imageContentLength,
1307 + _In_opt_ const WslcLoadImageOptions* options,
1308 + _Outptr_opt_result_z_ PWSTR* errorMessage)
1309 +try
1310 +{
1311 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1312 + auto internalType = CheckAndGetInternalType(session);
1313 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1314 + return WslcLoadSessionImageImpl(internalType, options, errorInfoWrapper, {imageContent, imageContentLength});
1315 +}
1316 +CATCH_RETURN();
1317 +
1318 +STDAPI WslcLoadSessionImageFromFile(_In_ WslcSession session, _In_z_ PCWSTR path, _In_opt_ const WslcLoadImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage)
1319 +try
1320 +{
1321 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1322 + auto internalType = CheckAndGetInternalType(session);
1323 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1324 + return WslcLoadSessionImageImpl(internalType, options, errorInfoWrapper, {path});
1325 +}
1326 +CATCH_RETURN();
1327 +
1328 +STDAPI WslcDeleteSessionImage(_In_ WslcSession session, _In_z_ PCSTR nameOrID, _Outptr_opt_result_z_ PWSTR* errorMessage)
1329 +try
1330 +{
1331 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1332 + auto internalType = CheckAndGetInternalType(session);
1333 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1334 + RETURN_HR_IF_NULL(E_POINTER, nameOrID);
1335 +
1336 + WSLCDeleteImageOptions options{};
1337 + options.Image = nameOrID;
1338 + // TODO: Flags? (Force and NoPrune)
1339 +
1340 + wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImageInformation;
1341 +
1342 + return errorInfoWrapper.CaptureResult(
1343 + internalType->session->DeleteImage(&options, &deletedImageInformation, deletedImageInformation.size_address<ULONG>()));
1344 +}
1345 +CATCH_RETURN();
1346 +
1347 +STDAPI WslcTagSessionImage(_In_ WslcSession session, _In_ const WslcTagImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage)
1348 +try
1349 +{
1350 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1351 + auto internalType = CheckAndGetInternalType(session);
1352 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1353 + RETURN_HR_IF_NULL(E_POINTER, options);
1354 + RETURN_HR_IF_NULL(E_INVALIDARG, options->image);
1355 + RETURN_HR_IF_NULL(E_INVALIDARG, options->repo);
1356 + RETURN_HR_IF_NULL(E_INVALIDARG, options->tag);
1357 +
1358 + WSLCTagImageOptions runtimeOptions{};
1359 + runtimeOptions.Image = options->image;
1360 + runtimeOptions.Repo = options->repo;
1361 + runtimeOptions.Tag = options->tag;
1362 +
1363 + return errorInfoWrapper.CaptureResult(internalType->session->TagImage(&runtimeOptions));
1364 +}
1365 +CATCH_RETURN();
1366 +
1367 +STDAPI WslcPushSessionImage(_In_ WslcSession session, _In_ const WslcPushImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage)
1368 +try
1369 +{
1370 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1371 + auto internalType = CheckAndGetInternalType(session);
1372 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1373 + RETURN_HR_IF_NULL(E_POINTER, options);
1374 + RETURN_HR_IF_NULL(E_INVALIDARG, options->image);
1375 + RETURN_HR_IF_NULL(E_INVALIDARG, options->registryAuth);
1376 +
1377 + auto progressCallback = ProgressCallback::CreateIf(options);
1378 +
1379 + return errorInfoWrapper.CaptureResult(internalType->session->PushImage(options->image, options->registryAuth, progressCallback.get()));
1380 +}
1381 +CATCH_RETURN();
1382 +
1383 +STDAPI WslcSessionAuthenticate(
1384 + _In_ WslcSession session,
1385 + _In_z_ PCSTR serverAddress,
1386 + _In_z_ PCSTR username,
1387 + _In_z_ PCSTR password,
1388 + _Outptr_result_z_ PSTR* identityToken,
1389 + _Outptr_opt_result_z_ PWSTR* errorMessage)
1390 +try
1391 +{
1392 + ErrorInfoWrapper errorInfoWrapper{errorMessage};
1393 + auto internalType = CheckAndGetInternalType(session);
1394 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1395 + RETURN_HR_IF_NULL(E_POINTER, serverAddress);
1396 + RETURN_HR_IF_NULL(E_POINTER, username);
1397 + RETURN_HR_IF_NULL(E_POINTER, password);
1398 + RETURN_HR_IF_NULL(E_POINTER, identityToken);
1399 +
1400 + *identityToken = nullptr;
1401 +
1402 + wil::unique_cotaskmem_ansistring token;
1403 + auto hr = errorInfoWrapper.CaptureResult(internalType->session->Authenticate(serverAddress, username, password, &token));
1404 + if (SUCCEEDED(hr))
1405 + {
1406 + *identityToken = token.release();
1407 + }
1408 +
1409 + return errorInfoWrapper;
1410 +}
1411 +CATCH_RETURN();
1412 +
1413 +STDAPI WslcListSessionImages(_In_ WslcSession session, _Outptr_result_buffer_(*count) WslcImageInfo** images, _Out_ uint32_t* count)
1414 +try
1415 +{
1416 + static_assert(
1417 + sizeof(decltype(WslcImageInfo::name)) == sizeof(decltype(WSLCImageInformation::Image)), "Image name size mismatch.");
1418 +
1419 + RETURN_HR_IF_NULL(E_POINTER, images);
1420 + *images = nullptr;
1421 + RETURN_HR_IF_NULL(E_POINTER, count);
1422 + *count = 0;
1423 + auto internalType = CheckAndGetInternalType(session);
1424 + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session);
1425 +
1426 + // TODO: Many filtering options are available via WSLC_LIST_IMAGES_OPTIONS
1427 +
1428 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> imageInformation;
1429 +
1430 + RETURN_IF_FAILED(internalType->session->ListImages(nullptr, &imageInformation, imageInformation.size_address<ULONG>()));
1431 +
1432 + if (imageInformation.size())
1433 + {
1434 + auto result = wil::make_unique_cotaskmem<WslcImageInfo[]>(imageInformation.size());
1435 +
1436 + for (size_t i = 0; i < imageInformation.size(); ++i)
1437 + {
1438 + WslcImageInfo& currentResult = result[i];
1439 + WSLCImageInformation& currentImage = imageInformation[i];
1440 +
1441 + static_assert(std::is_trivial_v<WslcImageInfo>, "WslcImageInfo must be trivial.");
1442 + currentResult = {};
1443 +
1444 + THROW_HR_IF(
1445 + E_UNEXPECTED,
1446 + memcpy_s(currentResult.name, sizeof(decltype(WslcImageInfo::name)), currentImage.Image, sizeof(decltype(WSLCImageInformation::Image))) !=
1447 + 0);
1448 + ConvertSHA256Hash(currentImage.Hash, currentResult.sha256);
1449 + currentResult.sizeBytes = currentImage.Size;
1450 + currentResult.createdUnixTime = currentImage.Created;
1451 + }
1452 +
1453 + *images = result.release();
1454 + *count = static_cast<uint32_t>(imageInformation.size());
1455 + }
1456 +
1457 + return S_OK;
1458 +}
1459 +CATCH_RETURN();
1460 +
1461 +// STORAGE
1462 +
1463 +// INSTALL
1464 +
1465 +STDAPI WslcGetMissingComponents(_Out_ WslcComponentFlags* missingComponents)
1466 +try
1467 +{
1468 + RETURN_HR_IF_NULL(E_POINTER, missingComponents);
1469 +
1470 + *missingComponents = WSLC_COMPONENT_FLAG_NONE;
1471 +
1472 + WslcComponentFlags componentCheck = WSLC_COMPONENT_FLAG_NONE;
1473 +
1474 + WI_SetFlagIf(componentCheck, WSLC_COMPONENT_FLAG_VIRTUAL_MACHINE_PLATFORM, NeedsVirtualMachineServicesInstalled());
1475 + WI_SetFlagIf(componentCheck, WSLC_COMPONENT_FLAG_WSL_PACKAGE, NeedsWslRuntimeInstalled());
1476 +
1477 + *missingComponents = componentCheck;
1478 +
1479 + return S_OK;
1480 +}
1481 +CATCH_RETURN();
1482 +
1483 +STDAPI WslcGetVersion(_Out_writes_(1) WslcVersion* version)
1484 +try
1485 +{
1486 + RETURN_HR_IF_NULL(E_POINTER, version);
1487 +
1488 + static_assert(std::is_trivial_v<WslcVersion>, "WslcVersion must be trivial");
1489 + *version = {};
1490 +
1491 + wil::com_ptr<IWSLCSessionManager> sessionManager = CreateSessionManager();
1492 +
1493 + WSLCVersion runtimeVersion{};
1494 + RETURN_IF_FAILED(sessionManager->GetVersion(&runtimeVersion));
1495 +
1496 + version->major = runtimeVersion.Major;
1497 + version->minor = runtimeVersion.Minor;
1498 + version->revision = runtimeVersion.Revision;
1499 +
1500 + return S_OK;
1501 +}
1502 +CATCH_RETURN();
1503 +
1504 +STDAPI WslcInstallWithDependencies(_In_opt_ WslcInstallCallback progressCallback, _In_opt_ PVOID context)
1505 +try
1506 +{
1507 + HRESULT result = S_OK;
1508 + bool needsVirtualMachine = NeedsVirtualMachineServicesInstalled();
1509 + bool needsRuntime = NeedsWslRuntimeInstalled();
1510 +
1511 + if (!needsVirtualMachine && !needsRuntime)
1512 + {
1513 + return result;
1514 + }
1515 +
1516 + // Installing these components requires elevation.
1517 + auto token = wil::open_current_access_token();
1518 + RETURN_HR_IF(
1519 + HRESULT_FROM_WIN32(ERROR_ELEVATION_REQUIRED),
1520 + !wsl::windows::common::security::IsTokenElevated(token.get()) && !wsl::windows::common::security::IsTokenLocalSystem(token.get()));
1521 +
1522 + if (needsVirtualMachine)
1523 + {
1524 + if (progressCallback)
1525 + {
1526 + progressCallback(WSLC_COMPONENT_FLAG_VIRTUAL_MACHINE_PLATFORM, 0, 1, context);
1527 + }
1528 +
1529 + auto exitCode = WslInstall::InstallOptionalComponent(WslInstall::c_optionalFeatureNameVmp, false);
1530 + if (exitCode == ERROR_SUCCESS_REBOOT_REQUIRED)
1531 + {
1532 + result = HRESULT_FROM_WIN32(ERROR_SUCCESS_REBOOT_REQUIRED);
1533 + }
1534 + else if (exitCode != 0)
1535 + {
1536 + THROW_HR_WITH_USER_ERROR(
1537 + WSL_E_INSTALL_COMPONENT_FAILED,
1538 + wsl::shared::Localization::MessageOptionalComponentInstallFailed(WslInstall::c_optionalFeatureNameVmp, exitCode));
1539 + }
1540 +
1541 + if (progressCallback)
1542 + {
1543 + progressCallback(WSLC_COMPONENT_FLAG_VIRTUAL_MACHINE_PLATFORM, 1, 1, context);
1544 + }
1545 + }
1546 +
1547 + if (needsRuntime)
1548 + {
1549 + std::function<void(uint32_t)> callback;
1550 + if (progressCallback)
1551 + {
1552 + callback = [progressCallback, context](uint32_t progress) {
1553 + progressCallback(WSLC_COMPONENT_FLAG_WSL_PACKAGE, progress, 100, context);
1554 + };
1555 + }
1556 +
1557 + wsl::windows::common::WindowsUpdateContext wuContext;
1558 + wuContext.RunUpdateFlow(true, callback);
1559 + }
1560 +
1561 + return result;
1562 +}
1563 +CATCH_RETURN();
1564 +
1565 +EXTERN_C BOOL STDAPICALLTYPE DllMain(_In_ HINSTANCE Instance, _In_ DWORD Reason, _In_opt_ LPVOID Reserved)
1566 +{
1567 + wil::DLLMain(Instance, Reason, Reserved);
1568 +
1569 + switch (Reason)
1570 + {
1571 + case DLL_PROCESS_ATTACH:
1572 + wsl::windows::common::wslutil::InitializeWil();
1573 + WslTraceLoggingInitialize(WslcTelemetryProvider, false);
1574 + break;
1575 +
1576 + case DLL_PROCESS_DETACH:
1577 + WslTraceLoggingUninitialize();
1578 + break;
1579 + }
1580 +
1581 + return TRUE;
1582 +}
src/windows/WslcSDK/wslcsdk.def new
+77
@@ -0,0 +1,77 @@
1 +LIBRARY wslcsdk
2 +
3 +EXPORTS
4 +
5 +DllMain
6 +
7 +WslcInitSessionSettings
8 +WslcInitContainerSettings
9 +WslcInitProcessSettings
10 +
11 +WslcCreateSession
12 +WslcCreateContainer
13 +
14 +WslcReleaseSession
15 +WslcReleaseContainer
16 +WslcReleaseProcess
17 +
18 +WslcSetSessionSettingsFeatureFlags
19 +WslcSetSessionSettingsTerminationCallback
20 +WslcSetSessionSettingsCpuCount
21 +WslcSetSessionSettingsMemory
22 +WslcSetSessionSettingsTimeout
23 +WslcSetSessionSettingsVhd
24 +
25 +WslcTerminateSession
26 +WslcSessionAuthenticate
27 +WslcPullSessionImage
28 +WslcImportSessionImage
29 +WslcImportSessionImageFromFile
30 +WslcLoadSessionImage
31 +WslcLoadSessionImageFromFile
32 +WslcDeleteSessionImage
33 +WslcListSessionImages
34 +WslcCreateSessionVhdVolume
35 +WslcDeleteSessionVhdVolume
36 +WslcTagSessionImage
37 +WslcPushSessionImage
38 +
39 +WslcSetContainerSettingsDomainName
40 +WslcSetContainerSettingsName
41 +WslcSetContainerSettingsNetworkingMode
42 +WslcSetContainerSettingsHostName
43 +WslcSetContainerSettingsVolumes
44 +WslcSetContainerSettingsNamedVolumes
45 +WslcSetContainerSettingsInitProcess
46 +WslcSetContainerSettingsFlags
47 +WslcSetContainerSettingsPortMappings
48 +
49 +WslcCreateContainerProcess
50 +WslcStartContainer
51 +WslcInspectContainer
52 +WslcGetContainerState
53 +WslcStopContainer
54 +WslcDeleteContainer
55 +WslcGetContainerID
56 +WslcGetContainerInitProcess
57 +
58 +WslcSetProcessSettingsCallbacks
59 +WslcSetProcessSettingsWorkingDirectory
60 +WslcSetProcessSettingsCmdLine
61 +WslcSetProcessSettingsEnvVariables
62 +WslcGetProcessPid
63 +WslcGetProcessExitEvent
64 +WslcGetProcessState
65 +WslcGetProcessExitCode
66 +WslcSignalProcess
67 +WslcGetProcessIOHandle
68 +
69 +WslcGetMissingComponents
70 +WslcGetVersion
71 +WslcInstallWithDependencies
72 +
73 +
74 +
75 +
76 +
77 +
src/windows/WslcSDK/wslcsdk.h new
+570
@@ -0,0 +1,570 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WslcSDK.h
8 +
9 +Abstract:
10 +
11 + This file contains the public WSL Container SDK api definitions.
12 +
13 + PREVIEW NOTICE: This API is currently in preview and is subject to breaking
14 + changes in future releases without prior notice. Do not rely on API stability
15 + for production workloads. Features, function signatures, and behaviors may
16 + change between releases during the preview period.
17 +
18 +--*/
19 +#pragma once
20 +#include <winsock2.h>
21 +#include <ws2tcpip.h>
22 +#include <windows.h>
23 +#include <stdint.h>
24 +#include <specstrings.h>
25 +
26 +EXTERN_C_START
27 +
28 +// Session values
29 +#define WSLC_SESSION_OPTIONS_SIZE 80
30 +#define WSLC_SESSION_OPTIONS_ALIGNMENT 8
31 +
32 +typedef struct WslcSessionSettings
33 +{
34 + __declspec(align(WSLC_SESSION_OPTIONS_ALIGNMENT)) BYTE _opaque[WSLC_SESSION_OPTIONS_SIZE];
35 +} WslcSessionSettings;
36 +
37 +DECLARE_HANDLE(WslcSession);
38 +
39 +// Container values
40 +#define WSLC_CONTAINER_OPTIONS_SIZE 96
41 +#define WSLC_CONTAINER_OPTIONS_ALIGNMENT 8
42 +
43 +typedef struct WslcContainerSettings
44 +{
45 + __declspec(align(WSLC_CONTAINER_OPTIONS_ALIGNMENT)) BYTE _opaque[WSLC_CONTAINER_OPTIONS_SIZE];
46 +} WslcContainerSettings;
47 +
48 +DECLARE_HANDLE(WslcContainer);
49 +
50 +// Process values
51 +#define WSLC_CONTAINER_PROCESS_OPTIONS_SIZE 72
52 +#define WSLC_CONTAINER_PROCESS_OPTIONS_ALIGNMENT 8
53 +typedef struct WslcProcessSettings
54 +{
55 + __declspec(align(WSLC_CONTAINER_PROCESS_OPTIONS_ALIGNMENT)) BYTE _opaque[WSLC_CONTAINER_PROCESS_OPTIONS_SIZE];
56 +} WslcProcessSettings;
57 +
58 +DECLARE_HANDLE(WslcProcess);
59 +
60 +typedef enum WslcContainerNetworkingMode
61 +{
62 + WSLC_CONTAINER_NETWORKING_MODE_NONE = 0, // No networking / isolated
63 + WSLC_CONTAINER_NETWORKING_MODE_BRIDGED = 1
64 +} WslcContainerNetworkingMode;
65 +
66 +typedef enum WslcVhdType
67 +{
68 + WSLC_VHD_TYPE_DYNAMIC = 0, // Expanding VHDX (default)
69 + WSLC_VHD_TYPE_FIXED = 1
70 +} WslcVhdType;
71 +
72 +typedef struct WslcVhdRequirements
73 +{
74 + // Ignored by WslcSetSessionSettingsVhd
75 + _In_z_ PCSTR name;
76 + _In_ uint64_t sizeBytes; // Desired size (for create/expand)
77 + _In_ WslcVhdType type;
78 +} WslcVhdRequirements;
79 +
80 +typedef enum WslcSessionFeatureFlags
81 +{
82 + WSLC_SESSION_FEATURE_FLAG_NONE = 0x00000000,
83 + WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU = 0x00000004
84 +} WslcSessionFeatureFlags;
85 +
86 +DEFINE_ENUM_FLAG_OPERATORS(WslcSessionFeatureFlags);
87 +
88 +typedef enum WslcSessionTerminationReason
89 +{
90 + WSLC_SESSION_TERMINATION_REASON_UNKNOWN = 0,
91 + WSLC_SESSION_TERMINATION_REASON_SHUTDOWN = 1,
92 + WSLC_SESSION_TERMINATION_REASON_CRASHED = 2,
93 +} WslcSessionTerminationReason;
94 +
95 +typedef __callback void(CALLBACK* WslcSessionTerminationCallback)(_In_ WslcSessionTerminationReason reason, _In_opt_ PVOID context);
96 +
97 +STDAPI WslcInitSessionSettings(_In_ PCWSTR name, _In_ PCWSTR storagePath, _Out_ WslcSessionSettings* sessionSettings);
98 +
99 +STDAPI WslcCreateSession(_In_ WslcSessionSettings* sessionSettings, _Out_ WslcSession* session, _Outptr_opt_result_z_ PWSTR* errorMessage);
100 +
101 +// OPTIONAL SESSION SETTINGS
102 +STDAPI WslcSetSessionSettingsCpuCount(_In_ WslcSessionSettings* sessionSettings, _In_ uint32_t cpuCount);
103 +STDAPI WslcSetSessionSettingsMemory(_In_ WslcSessionSettings* sessionSettings, _In_ uint32_t memoryMB);
104 +STDAPI WslcSetSessionSettingsTimeout(_In_ WslcSessionSettings* sessionSettings, _In_ uint32_t timeoutMS);
105 +
106 +STDAPI WslcSetSessionSettingsVhd(_In_ WslcSessionSettings* sessionSettings, _In_opt_ const WslcVhdRequirements* vhdRequirements);
107 +
108 +STDAPI WslcSetSessionSettingsFeatureFlags(_In_ WslcSessionSettings* sessionSettings, _In_ WslcSessionFeatureFlags flags);
109 +
110 +// Pass in Null for callback to clear the termination callback
111 +STDAPI WslcSetSessionSettingsTerminationCallback(
112 + _In_ WslcSessionSettings* sessionSettings, _In_opt_ WslcSessionTerminationCallback terminationCallback, _In_opt_ PVOID terminationContext);
113 +
114 +STDAPI WslcTerminateSession(_In_ WslcSession session);
115 +STDAPI WslcReleaseSession(_In_ WslcSession session);
116 +
117 +// CONTAINER DEFINITIONS
118 +
119 +typedef enum WslcPortProtocol
120 +{
121 + WSLC_PORT_PROTOCOL_TCP = 0,
122 + WSLC_PORT_PROTOCOL_UDP = 1
123 +} WslcPortProtocol;
124 +
125 +typedef struct WslcContainerPortMapping
126 +{
127 + _In_ uint16_t windowsPort; // Port on Windows host
128 + _In_ uint16_t containerPort; // Port inside container
129 + _In_ WslcPortProtocol protocol; // TCP or UDP
130 +
131 + // if you want to override the default binding address
132 + _In_opt_ struct sockaddr_storage* windowsAddress; // accepts ipv4/6
133 +} WslcContainerPortMapping;
134 +
135 +typedef struct WslcContainerVolume
136 +{
137 + _In_z_ PCWSTR windowsPath;
138 + _In_z_ PCSTR containerPath;
139 + _In_ BOOL readOnly;
140 +} WslcContainerVolume;
141 +
142 +typedef struct WslcContainerNamedVolume
143 +{
144 + _In_z_ PCSTR name; // Name of the session volume (from WslcVhdRequirements.name)
145 + _In_z_ PCSTR containerPath; // Absolute path inside the container
146 + _In_ BOOL readOnly;
147 +} WslcContainerNamedVolume;
148 +
149 +typedef enum WslcContainerFlags
150 +{
151 + WSLC_CONTAINER_FLAG_NONE = 0x00000000,
152 + WSLC_CONTAINER_FLAG_AUTO_REMOVE = 0x00000001,
153 + WSLC_CONTAINER_FLAG_ENABLE_GPU = 0x00000002,
154 + WSLC_CONTAINER_FLAG_PRIVILEGED = 0x00000004,
155 +
156 +} WslcContainerFlags;
157 +
158 +DEFINE_ENUM_FLAG_OPERATORS(WslcContainerFlags);
159 +
160 +typedef enum WslcContainerStartFlags
161 +{
162 + WSLC_CONTAINER_START_FLAG_NONE = 0x00000000,
163 + WSLC_CONTAINER_START_FLAG_ATTACH = 0x00000001,
164 +
165 +} WslcContainerStartFlags;
166 +
167 +DEFINE_ENUM_FLAG_OPERATORS(WslcContainerStartFlags);
168 +
169 +STDAPI WslcInitContainerSettings(_In_ PCSTR imageName, _Out_ WslcContainerSettings* containerSettings);
170 +
171 +STDAPI WslcCreateContainer(_In_ WslcSession session, _In_ const WslcContainerSettings* containerSettings, _Out_ WslcContainer* container, _Outptr_opt_result_z_ PWSTR* errorMessage);
172 +
173 +STDAPI WslcStartContainer(_In_ WslcContainer container, _In_ WslcContainerStartFlags flags, _Outptr_opt_result_z_ PWSTR* errorMessage);
174 +
175 +// OPTIONAL CONTAINER SETTINGS
176 +STDAPI WslcSetContainerSettingsName(_In_ WslcContainerSettings* containerSettings, _In_ PCSTR name);
177 +
178 +STDAPI WslcSetContainerSettingsInitProcess(_In_ WslcContainerSettings* containerSettings, _In_ WslcProcessSettings* initProcess);
179 +
180 +STDAPI WslcSetContainerSettingsNetworkingMode(_In_ WslcContainerSettings* containerSettings, _In_ WslcContainerNetworkingMode networkingMode);
181 +
182 +STDAPI WslcSetContainerSettingsHostName(_In_ WslcContainerSettings* containerSettings, _In_ PCSTR hostName);
183 +
184 +STDAPI WslcSetContainerSettingsDomainName(_In_ WslcContainerSettings* containerSettings, _In_ PCSTR domainName);
185 +
186 +STDAPI WslcSetContainerSettingsFlags(_In_ WslcContainerSettings* containerSettings, _In_ WslcContainerFlags flags);
187 +
188 +STDAPI WslcSetContainerSettingsPortMappings(
189 + _In_ WslcContainerSettings* containerSettings,
190 + _In_reads_opt_(portMappingCount) const WslcContainerPortMapping* portMappings,
191 + _In_ uint32_t portMappingCount);
192 +
193 +// Add the container volumes to the volumes array
194 +STDAPI WslcSetContainerSettingsVolumes(
195 + _In_ WslcContainerSettings* containerSettings, _In_reads_opt_(volumeCount) const WslcContainerVolume* volumes, _In_ uint32_t volumeCount);
196 +
197 +// Add named session volumes (created via WslcCreateSessionVhdVolume) to the container settings
198 +STDAPI WslcSetContainerSettingsNamedVolumes(
199 + _In_ WslcContainerSettings* containerSettings,
200 + _In_reads_opt_(namedVolumeCount) const WslcContainerNamedVolume* namedVolumes,
201 + _In_ uint32_t namedVolumeCount);
202 +
203 +STDAPI WslcCreateContainerProcess(
204 + _In_ WslcContainer container, _In_ WslcProcessSettings* newProcessSettings, _Out_ WslcProcess* newProcess, _Outptr_opt_result_z_ PWSTR* errorMessage);
205 +
206 +STDAPI WslcReleaseContainer(_In_ WslcContainer container);
207 +
208 +// GENERAL CONTAINER MANAGEMENT
209 +
210 +#define WSLC_CONTAINER_ID_BUFFER_SIZE 65 // 64 hex chars + null terminator
211 +
212 +STDAPI WslcGetContainerID(_In_ WslcContainer container, _Out_writes_(WSLC_CONTAINER_ID_BUFFER_SIZE) CHAR containerID[WSLC_CONTAINER_ID_BUFFER_SIZE]);
213 +
214 +STDAPI WslcGetContainerInitProcess(_In_ WslcContainer container, _Out_ WslcProcess* initProcess);
215 +
216 +// Retrieves the inspection data for a container.
217 +//
218 +// Parameters:
219 +// container
220 +// A valid WslcContainer handle representing the container to inspect.
221 +//
222 +// inspectData
223 +// On success, receives a pointer to a null-terminated ANSI string
224 +// containing the inspection data.
225 +//
226 +// The string is allocated using CoTaskMemAlloc. The caller takes
227 +// ownership of the returned memory and must free it by calling
228 +// CoTaskMemFree when it is no longer needed.
229 +//
230 +// Return Value:
231 +// S_OK on success. Otherwise, an HRESULT error code indicating the failure.
232 +STDAPI WslcInspectContainer(_In_ WslcContainer container, _Outptr_result_z_ PSTR* inspectData);
233 +
234 +typedef enum WslcContainerState
235 +{
236 + WSLC_CONTAINER_STATE_INVALID = 0,
237 + WSLC_CONTAINER_STATE_CREATED = 1,
238 + WSLC_CONTAINER_STATE_RUNNING = 2,
239 + WSLC_CONTAINER_STATE_EXITED = 3,
240 + WSLC_CONTAINER_STATE_DELETED = 4,
241 +} WslcContainerState;
242 +
243 +STDAPI WslcGetContainerState(_In_ WslcContainer container, _Out_ WslcContainerState* state);
244 +
245 +// Will define more signals as needed:
246 +typedef enum WslcSignal
247 +{
248 + WSLC_SIGNAL_NONE = 0, // No signal; reserved for future use
249 + WSLC_SIGNAL_SIGHUP = 1, // SIGHUP: reload / hangup
250 + WSLC_SIGNAL_SIGINT = 2, // SIGINT: interrupt (Ctrl-C)
251 + WSLC_SIGNAL_SIGQUIT = 3, // SIGQUIT: quit with core dump
252 + WSLC_SIGNAL_SIGKILL = 9, // SIGKILL: immediate termination
253 + WSLC_SIGNAL_SIGTERM = 15, // SIGTERM: graceful shutdown
254 +} WslcSignal;
255 +
256 +STDAPI WslcStopContainer(_In_ WslcContainer container, _In_ WslcSignal signal, _In_ uint32_t timeoutSeconds, _Outptr_opt_result_z_ PWSTR* errorMessage);
257 +
258 +typedef enum WslcDeleteContainerFlags
259 +{
260 + WSLC_DELETE_CONTAINER_FLAG_NONE = 0,
261 + WSLC_DELETE_CONTAINER_FLAG_FORCE = 0x01
262 +} WslcDeleteContainerFlags;
263 +
264 +DEFINE_ENUM_FLAG_OPERATORS(WslcDeleteContainerFlags);
265 +
266 +STDAPI WslcDeleteContainer(_In_ WslcContainer container, _In_ WslcDeleteContainerFlags flags, _Outptr_opt_result_z_ PWSTR* errorMessage);
267 +
268 +// PROCESS DEFINITIONS
269 +STDAPI WslcInitProcessSettings(_Out_ WslcProcessSettings* processSettings);
270 +
271 +// OPTIONAL PROCESS SETTINGS
272 +
273 +STDAPI WslcSetProcessSettingsWorkingDirectory(_In_ WslcProcessSettings* processSettings, _In_ PCSTR workingDirectory);
274 +
275 +STDAPI WslcSetProcessSettingsCmdLine(_In_ WslcProcessSettings* processSettings, _In_reads_(argc) PCSTR const* argv, size_t argc);
276 +
277 +STDAPI WslcSetProcessSettingsEnvVariables(_In_ WslcProcessSettings* processSettings, _In_reads_(argc) PCSTR const* key_value, size_t argc);
278 +
279 +typedef enum WslcProcessIOHandle
280 +{
281 + WSLC_PROCESS_IO_HANDLE_STDIN = 0,
282 + WSLC_PROCESS_IO_HANDLE_STDOUT = 1,
283 + WSLC_PROCESS_IO_HANDLE_STDERR = 2
284 +} WslcProcessIOHandle;
285 +
286 +// Callback invoked when stdout or stderr data is available from a running
287 +// WSLC process.
288 +//
289 +// Parameters:
290 +// ioHandle
291 +// The WslcProcessIOHandle that the IO callback is for.
292 +// Only STDOUT and STDERR will receive callbacks.
293 +//
294 +// data
295 +// Pointer to a buffer containing the bytes read. The buffer is owned
296 +// by WSLC and is valid only for the duration of the callback.
297 +//
298 +// The caller must not free, modify, or retain the pointer. If the
299 +// caller needs to keep the data, it must copy the contents before
300 +// returning from the callback.
301 +//
302 +// dataBytes
303 +// Number of bytes available in the data buffer.
304 +//
305 +// context
306 +// Caller-supplied context pointer that was provided when the callback
307 +// was registered.
308 +//
309 +// Notes:
310 +// - WSLC frees or reuses the buffer immediately after the callback returns.
311 +// - The callback must return promptly; long-running operations may block
312 +// WSLC's internal I/O processing.
313 +// - The buffer is not null-terminated; it is a raw byte sequence.
314 +//
315 +typedef __callback void(CALLBACK* WslcStdIOCallback)(
316 + WslcProcessIOHandle ioHandle, _In_reads_bytes_(dataBytes) const BYTE* data, _In_ uint32_t dataBytes, _In_opt_ PVOID context);
317 +
318 +// Callback invoked when a WSLC process has exited AND any remaining IO has been flushed.
319 +//
320 +// Parameters:
321 +// exitCode
322 +// The exit code of the process.
323 +//
324 +// context
325 +// Caller-supplied context pointer that was provided when the callback
326 +// was registered.
327 +//
328 +// Notes:
329 +// - Once this callback is invoked, any registered IO callbacks will no longer be called.
330 +//
331 +typedef __callback void(CALLBACK* WslcProcessExitCallback)(INT32 exitCode, _In_opt_ PVOID context);
332 +
333 +// Using any callbacks will consume the IO handles, preventing acquisition through WslcGetProcessIOHandle.
334 +// If using IO callbacks, also use the exit callback to prevent a race between process exit and IO buffer flushing.
335 +typedef struct WslcProcessCallbacks
336 +{
337 + WslcStdIOCallback onStdOut;
338 + WslcStdIOCallback onStdErr;
339 + WslcProcessExitCallback onExit;
340 +} WslcProcessCallbacks;
341 +
342 +STDAPI WslcSetProcessSettingsCallbacks(_In_ WslcProcessSettings* processSettings, _In_ const WslcProcessCallbacks* callbacks, _In_opt_ PVOID context);
343 +
344 +// PROCESS MANAGEMENT
345 +
346 +STDAPI WslcGetProcessPid(_In_ WslcProcess process, _Out_ uint32_t* pid);
347 +
348 +STDAPI WslcGetProcessExitEvent(_In_ WslcProcess process, _Out_ HANDLE* exitEvent);
349 +
350 +typedef enum WslcProcessState
351 +{
352 + WSLC_PROCESS_STATE_UNKNOWN = 0,
353 + WSLC_PROCESS_STATE_RUNNING = 1,
354 + WSLC_PROCESS_STATE_EXITED = 2,
355 + WSLC_PROCESS_STATE_SIGNALLED = 3
356 +} WslcProcessState;
357 +
358 +STDAPI WslcGetProcessState(_In_ WslcProcess process, _Out_ WslcProcessState* state);
359 +
360 +STDAPI WslcGetProcessExitCode(_In_ WslcProcess process, _Out_ PINT32 exitCode);
361 +
362 +STDAPI WslcSignalProcess(_In_ WslcProcess process, _In_ WslcSignal signal);
363 +
364 +STDAPI WslcGetProcessIOHandle(_In_ WslcProcess process, _In_ WslcProcessIOHandle ioHandle, _Out_ HANDLE* handle);
365 +
366 +STDAPI WslcReleaseProcess(_In_ WslcProcess process);
367 +
368 +// IMAGE MANAGEMENT
369 +
370 +// Container image
371 +typedef struct WslcImageProgressDetail
372 +{
373 + _Out_ uint64_t currentBytes; // bytes downloaded so far
374 + _Out_ uint64_t totalBytes; // total bytes expected
375 +} WslcImageProgressDetail;
376 +
377 +typedef enum WslcImageProgressStatus
378 +{
379 + WSLC_IMAGE_PROGRESS_STATUS_UNKNOWN = 0,
380 + WSLC_IMAGE_PROGRESS_STATUS_PULLING = 1, // "Pulling fs layer"
381 + WSLC_IMAGE_PROGRESS_STATUS_WAITING = 2, // "Waiting"
382 + WSLC_IMAGE_PROGRESS_STATUS_DOWNLOADING = 3, // "Downloading"
383 + WSLC_IMAGE_PROGRESS_STATUS_VERIFYING = 4, // "Verifying Checksum"
384 + WSLC_IMAGE_PROGRESS_STATUS_EXTRACTING = 5, // "Extracting"
385 + WSLC_IMAGE_PROGRESS_STATUS_COMPLETE = 6 // "Pull complete"
386 +} WslcImageProgressStatus;
387 +
388 +typedef struct WslcImageProgressMessage
389 +{
390 + _Out_ PCSTR id; // layer ID or digest
391 + _Out_ WslcImageProgressStatus status; // "Downloading", "Extracting", etc.
392 + _Out_ WslcImageProgressDetail detail;
393 +} WslcImageProgressMessage;
394 +
395 +// pointer-to-function typedef (unambiguous)
396 +typedef HRESULT(CALLBACK* WslcContainerImageProgressCallback)(const WslcImageProgressMessage* progress, PVOID context);
397 +
398 +// options struct typedef is a pointer type and _In_opt_ is valid
399 +typedef struct WslcPullImageOptions
400 +{
401 + _In_z_ PCSTR uri;
402 + WslcContainerImageProgressCallback progressCallback;
403 + PVOID progressCallbackContext;
404 + _In_opt_z_ PCSTR registryAuth;
405 +} WslcPullImageOptions;
406 +
407 +STDAPI WslcPullSessionImage(_In_ WslcSession session, _In_ const WslcPullImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage);
408 +
409 +typedef struct WslcImportImageOptions
410 +{
411 + _In_opt_ WslcContainerImageProgressCallback progressCallback;
412 + _In_opt_ PVOID progressCallbackContext;
413 +} WslcImportImageOptions;
414 +
415 +STDAPI WslcImportSessionImage(
416 + _In_ WslcSession session,
417 + _In_z_ PCSTR imageName,
418 + _In_ HANDLE imageContent,
419 + _In_ uint64_t imageContentBytes,
420 + _In_opt_ const WslcImportImageOptions* options,
421 + _Outptr_opt_result_z_ PWSTR* errorMessage);
422 +
423 +STDAPI WslcImportSessionImageFromFile(
424 + _In_ WslcSession session, _In_z_ PCSTR imageName, _In_z_ PCWSTR path, _In_opt_ const WslcImportImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage);
425 +
426 +typedef struct WslcLoadImageOptions
427 +{
428 + _In_opt_ WslcContainerImageProgressCallback progressCallback;
429 + _In_opt_ PVOID progressCallbackContext;
430 +} WslcLoadImageOptions;
431 +
432 +STDAPI WslcLoadSessionImage(
433 + _In_ WslcSession session,
434 + _In_ HANDLE imageContent,
435 + _In_ uint64_t imageContentBytes,
436 + _In_opt_ const WslcLoadImageOptions* options,
437 + _Outptr_opt_result_z_ PWSTR* errorMessage);
438 +
439 +STDAPI WslcLoadSessionImageFromFile(
440 + _In_ WslcSession session, _In_z_ PCWSTR path, _In_opt_ const WslcLoadImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage);
441 +
442 +#define WSLC_IMAGE_NAME_LENGTH 256 // 255 chars + null
443 +
444 +typedef struct WslcImageInfo
445 +{
446 + // we should expose this
447 + CHAR name[WSLC_IMAGE_NAME_LENGTH];
448 + uint8_t sha256[32];
449 + uint64_t sizeBytes;
450 + uint64_t createdUnixTime;
451 +} WslcImageInfo;
452 +
453 +STDAPI WslcDeleteSessionImage(_In_ WslcSession session, _In_z_ PCSTR nameOrID, _Outptr_opt_result_z_ PWSTR* errorMessage);
454 +
455 +typedef struct WslcTagImageOptions
456 +{
457 + _In_z_ PCSTR image; // Source image name or ID.
458 + _In_z_ PCSTR repo; // Target repository name.
459 + _In_z_ PCSTR tag; // Target tag name.
460 +} WslcTagImageOptions;
461 +
462 +STDAPI WslcTagSessionImage(_In_ WslcSession session, _In_ const WslcTagImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage);
463 +
464 +typedef struct WslcPushImageOptions
465 +{
466 + _In_z_ PCSTR image;
467 + _In_z_ PCSTR registryAuth; // Base64-encoded X-Registry-Auth header value.
468 + _In_opt_ WslcContainerImageProgressCallback progressCallback;
469 + _In_opt_ PVOID progressCallbackContext;
470 +} WslcPushImageOptions;
471 +
472 +STDAPI WslcPushSessionImage(_In_ WslcSession session, _In_ const WslcPushImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage);
473 +
474 +// Authenticates with a container registry and returns an identity token.
475 +//
476 +// Parameters:
477 +// session
478 +// A valid WslcSession handle.
479 +//
480 +// serverAddress
481 +// The registry server address (e.g. "127.0.0.1:5000").
482 +//
483 +// username
484 +// The username for authentication.
485 +//
486 +// password
487 +// The password for authentication.
488 +//
489 +// identityToken
490 +// On success, receives a pointer to a null-terminated ANSI string
491 +// containing the identity token.
492 +//
493 +// The string is allocated using CoTaskMemAlloc. The caller takes
494 +// ownership of the returned memory and must free it by calling
495 +// CoTaskMemFree when it is no longer needed.
496 +//
497 +// Return Value:
498 +// S_OK on success. Otherwise, an HRESULT error code indicating the failure.
499 +STDAPI WslcSessionAuthenticate(
500 + _In_ WslcSession session,
501 + _In_z_ PCSTR serverAddress,
502 + _In_z_ PCSTR username,
503 + _In_z_ PCSTR password,
504 + _Outptr_result_z_ PSTR* identityToken,
505 + _Outptr_opt_result_z_ PWSTR* errorMessage);
506 +
507 +// Retrieves the list of container images
508 +// Parameters:
509 +// session
510 +// A valid WslcSession handle.
511 +//
512 +// images
513 +// On success, receives a pointer to a contiguous array of
514 +// WslcImageInfo structures describing the images
515 +//
516 +// The array is allocated using CoTaskMemAlloc. The caller takes
517 +// ownership of the memory and must free it by calling
518 +// CoTaskMemFree when it is no longer needed.
519 +//
520 +// count
521 +// On success, receives the number of elements in the images array.
522 +// On failure, *count is set to 0.
523 +//
524 +// Return Value:
525 +// S_OK on success. Otherwise, an HRESULT error code indicating the
526 +// reason for failure.
527 +//
528 +// Notes:
529 +// - The caller must pass non-null pointers for both 'images' and 'count'.
530 +//
531 +
532 +STDAPI WslcListSessionImages(_In_ WslcSession session, _Outptr_result_buffer_(*count) WslcImageInfo** images, _Out_ uint32_t* count);
533 +
534 +// STORAGE
535 +
536 +STDAPI WslcCreateSessionVhdVolume(_In_ WslcSession session, _In_ const WslcVhdRequirements* options, _Outptr_opt_result_z_ PWSTR* errorMessage);
537 +STDAPI WslcDeleteSessionVhdVolume(_In_ WslcSession session, _In_z_ PCSTR name, _Outptr_opt_result_z_ PWSTR* errorMessage);
538 +
539 +// INSTALL
540 +
541 +typedef enum WslcComponentFlags
542 +{
543 + WSLC_COMPONENT_FLAG_NONE = 0,
544 + // Services provided by the Virtual Machine Platform optional feature (other optional features may provide these services as
545 + // well). Installing this component will require a reboot.
546 + WSLC_COMPONENT_FLAG_VIRTUAL_MACHINE_PLATFORM = 1,
547 + // The WSL runtime package, at an appropriate version to provide support for WSLC.
548 + WSLC_COMPONENT_FLAG_WSL_PACKAGE = 2,
549 +} WslcComponentFlags;
550 +
551 +DEFINE_ENUM_FLAG_OPERATORS(WslcComponentFlags);
552 +
553 +STDAPI WslcGetMissingComponents(_Out_ WslcComponentFlags* missingComponents);
554 +
555 +typedef struct WslcVersion
556 +{
557 + uint32_t major;
558 + uint32_t minor;
559 + uint32_t revision;
560 +} WslcVersion;
561 +STDAPI WslcGetVersion(_Out_writes_(1) WslcVersion* version);
562 +
563 +typedef __callback void(CALLBACK* WslcInstallCallback)(
564 + _In_ WslcComponentFlags component, _In_ uint32_t progressSteps, _In_ uint32_t totalSteps, _In_opt_ PVOID context);
565 +
566 +// Callbacks will only be made for components that are actively installed by this call.
567 +// That list can be acquired prior to this call with `WslcCanRun`.
568 +STDAPI WslcInstallWithDependencies(_In_opt_ WslcInstallCallback progressCallback, _In_opt_ PVOID context);
569 +
570 +EXTERN_C_END
src/windows/common/CMakeLists.txt
+43 -24
@@ -8,6 +8,7 @@ set(SOURCES
8 Dmesg.cpp
9 DnsResolver.cpp
10 DnsTunnelingChannel.cpp
11 + ExecutionContext.cpp
12 filesystem.cpp
13 GnsChannel.cpp
14 GnsPortTrackerChannel.cpp
@@ -15,10 +16,8 @@ set(SOURCES
16 HandleConsoleProgressBar.cpp
17 hcs.cpp
18 helpers.cpp
18 - interop.cpp
19 - ExecutionContext.cpp
20 - socket.cpp
19 hvsocket.cpp
20 + interop.cpp
21 Localization.cpp
22 lxssbusclient.cpp
23 lxssclient.cpp
@@ -26,13 +25,19 @@ set(SOURCES
25 LxssSecurity.cpp
26 LxssServerPort.cpp
27 NatNetworking.cpp
28 + notifications.cpp
29 Redirector.cpp
30 registry.cpp
31 relay.cpp
32 RingBuffer.cpp
33 + socket.cpp
34 string.cpp
35 SubProcess.cpp
36 svccomm.cpp
37 + WindowsUpdateIntegration.cpp
38 + WSLCContainerLauncher.cpp
39 + VirtioNetworking.cpp
40 + WSLCProcessLauncher.cpp
41 WslClient.cpp
42 WslCoreConfig.cpp
43 WslCoreFilesystem.cpp
@@ -43,27 +48,15 @@ set(SOURCES
48 WslInstall.cpp
49 WslSecurity.cpp
50 WslTelemetry.cpp
46 - VirtioNetworking.cpp
51 wslutil.cpp
52 install.cpp
53 + WSLCUserSettings.cpp
54 notifications.cpp)
55
56 set(HEADERS
57 ../../../generated/Localization.h
53 - ../../shared/inc/CommandLine.h
54 - ../../shared/inc/defs.h
55 - ../../shared/inc/lxfsshares.h
56 - ../../shared/inc/lxinitshared.h
57 - ../../shared/inc/SocketChannel.h
58 - ../../shared/inc/socketshared.h
59 - ../../shared/inc/hns_schema.h
60 - ../../shared/inc/JsonUtils.h
61 - ../../shared/inc/stringshared.h
62 - ../../shared/inc/retryshared.h
63 - ../../shared/inc/message.h
64 - ../../shared/inc/prettyprintshared.h
65 - ../inc/WslPluginApi.h
66 - ../inc/wslpolicies.h
58 + ../inc/docker_schema.h
59 + ../inc/wslc_schema.h
60 ../inc/lxssbusclient.h
61 ../inc/lxssclient.h
62 ../inc/LxssDynamicFunction.h
@@ -72,7 +65,21 @@ set(HEADERS
65 ../inc/wsl.h
66 ../inc/wslconfig.h
67 ../inc/wslhost.h
68 + ../inc/wslpolicies.h
69 + ../inc/WslPluginApi.h
70 ../inc/wslrelay.h
71 + ../../shared/inc/CommandLine.h
72 + ../../shared/inc/defs.h
73 + ../../shared/inc/hns_schema.h
74 + ../../shared/inc/JsonUtils.h
75 + ../../shared/inc/lxfsshares.h
76 + ../../shared/inc/lxinitshared.h
77 + ../../shared/inc/message.h
78 + ../../shared/inc/prettyprintshared.h
79 + ../../shared/inc/retryshared.h
80 + ../../shared/inc/SocketChannel.h
81 + ../../shared/inc/socketshared.h
82 + ../../shared/inc/stringshared.h
83 ConsoleProgressBar.h
84 ConsoleProgressIndicator.h
85 ConsoleState.h
@@ -82,6 +89,7 @@ set(HEADERS
89 Dmesg.h
90 DnsResolver.h
91 DnsTunnelingChannel.h
92 + ExecutionContext.h
93 filesystem.hpp
94 GnsChannel.h
95 GnsPortTrackerChannel.h
@@ -90,25 +98,29 @@ set(HEADERS
98 hcs.hpp
99 hcs_schema.h
100 helpers.hpp
93 - interop.hpp
94 - ExecutionContext.h
95 - socket.hpp
101 hvsocket.hpp
102 INetworkingEngine.h
103 + interop.hpp
104 LxssMessagePort.h
105 LxssPort.h
106 LxssSecurity.h
107 LxssServerPort.h
108 NatNetworking.h
109 + notifications.h
110 precomp.h
111 Redirector.h
112 registry.hpp
113 relay.hpp
114 RingBuffer.h
115 + socket.hpp
116 string.hpp
117 Stringify.h
118 SubProcess.h
119 svccomm.hpp
120 + WindowsUpdateIntegration.h
121 + WSLCContainerLauncher.h
122 + VirtioNetworking.h
123 + WSLCProcessLauncher.h
124 WslClient.h
125 WslCoreConfig.h
126 WslCoreFilesystem.h
@@ -120,13 +132,20 @@ set(HEADERS
132 WslInstall.h
133 WslSecurity.h
134 WslTelemetry.h
123 - VirtioNetworking.h
135 wslutil.h
125 - notifications.h)
136 + EnumVariantMap.h
137 + WSLCUserSettings.h
138 + WSLCSessionDefaults.h
139 + )
140
141 add_library(common STATIC ${SOURCES} ${HEADERS})
128 -add_dependencies(common wslserviceidl localization wslservicemc wslinstalleridl)
142 +add_dependencies(common wslserviceidl localization wslservicemc wslinstalleridl yaml-cpp)
143
144 target_precompile_headers(common PRIVATE precomp.h)
145 set_target_properties(common PROPERTIES FOLDER windows)
146 target_include_directories(common PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/../service/mc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE})
147 +
148 +# WSLCUserSettings.cpp uses yaml-cpp headers.
149 +set_source_files_properties(WSLCUserSettings.cpp PROPERTIES
150 + INCLUDE_DIRECTORIES "${yaml-cpp_SOURCE_DIR}/include"
151 + COMPILE_DEFINITIONS "YAML_CPP_STATIC_DEFINE")
src/windows/common/COMImplClass.h new
+98
@@ -0,0 +1,98 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + COMImplClass.h
8 +
9 +Abstract:
10 +
11 + This file contains the definition for COMImplClass, a helper to forward calls from a COM class to an impl class.
12 + // N.B. This class allows multiple calls to happen in parallel, and only blocks Disconnect() until there are either no more callers, or only calling thread is in a call to the underlying class.
13 + // This is implemented that way so a caller can call Disconnect() from within a call to the COM class without causing a deadlock.
14 +
15 +--*/
16 +
17 +#pragma once
18 +
19 +namespace wsl::windows::service::wslc {
20 +
21 +template <typename TImpl>
22 +class COMImplClass
23 +{
24 +public:
25 + COMImplClass(TImpl* impl) : m_impl(impl)
26 + {
27 + }
28 +
29 + void Disconnect() noexcept
30 + {
31 + std::unique_lock lock(m_lock);
32 +
33 + // Only continue if either:
34 + // - There are no current callers
35 + // - This thread is the only caller
36 +
37 + m_cv.wait(lock, [this] {
38 + return m_callers.empty() || m_callers.size() == 1 && *m_callers.begin() == std::this_thread::get_id();
39 + });
40 +
41 + WI_ASSERT(m_impl != nullptr);
42 + m_impl = nullptr;
43 + }
44 +
45 +protected:
46 + template <typename... Args>
47 + HRESULT CallImpl(void (TImpl::*routine)(Args... args), Args... args)
48 + try
49 + {
50 + auto [lock, impl] = LockImpl();
51 + (impl->*routine)(std::forward<Args>(args)...);
52 +
53 + return S_OK;
54 + }
55 + CATCH_RETURN();
56 +
57 + template <typename... Args>
58 + HRESULT CallImpl(void (TImpl::*routine)(Args... args) const, Args... args)
59 + try
60 + {
61 + auto [lock, impl] = LockImpl();
62 + (impl->*routine)(std::forward<Args>(args)...);
63 +
64 + return S_OK;
65 + }
66 + CATCH_RETURN();
67 +
68 + [[nodiscard]] auto LockImpl()
69 + {
70 + // Check if m_impl is available and add ourselves to the list of callers if that's the case.
71 + {
72 + std::unique_lock lock{m_lock};
73 + THROW_HR_IF(RPC_E_DISCONNECTED, m_impl == nullptr);
74 +
75 + auto [_, inserted] = m_callers.insert(std::this_thread::get_id());
76 + WI_ASSERT(inserted);
77 + }
78 +
79 + auto release = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() {
80 + std::unique_lock lock{m_lock};
81 +
82 + auto removed = m_callers.erase(std::this_thread::get_id());
83 + WI_ASSERT(removed == 1);
84 +
85 + m_cv.notify_one();
86 + });
87 +
88 + return std::make_pair(std::move(release), m_impl);
89 + }
90 +
91 +private:
92 + std::mutex m_lock;
93 + std::condition_variable m_cv;
94 + _Guarded_by_(m_lock) std::unordered_set<std::thread::id> m_callers;
95 + TImpl* m_impl = nullptr;
96 +};
97 +
98 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/common/DeviceHostProxy.cpp
+291 -260
@@ -1,261 +1,292 @@
1 -// Copyright (C) Microsoft Corporation. All rights reserved.
2 -
3 -#include "precomp.h"
4 -#include "DeviceHostProxy.h"
5 -
6 -// This template works around a limitation with decltype on overloaded functions. It will be able
7 -// to get the correct version of GetVmWorkerProcess based on the provided type arguments. By
8 -// doing it this way, a compiler error will be generated if someone changes the signature of
9 -// GetVmWorkerProcess.
10 -//
11 -// The way this works: decltype(GetVmWorkerProcess) does not work because it's overloaded.
12 -// decltype(GetVmWorkerProcess(arg1, ...)) works to select an overload if you have values of the
13 -// correct type (std::declval<T>() generates a value of the specified type), however the result
14 -// of that is the function's return type, not the function's type, so the argument types must
15 -// be repeated to reconstruct the function type.
16 -template <typename... Args>
17 -using GetVmWorkerProcessType = decltype(GetVmWorkerProcess(std::declval<Args>()...))(Args...);
18 -
19 -// Limit the number of allowed doorbells registered by an external HDV vdev. Currently virtio-9p only uses
20 -// one doorbell and wsldevicehost uses only two.
21 -#define DEVICE_HOST_PROXY_DOORBELL_LIMIT 8
22 -
23 -using namespace wsl::windows::common::hcs;
24 -
25 -DeviceHostProxy::DeviceHostProxy(const std::wstring& VmId, const GUID& RuntimeId) :
26 - m_systemId{VmId}, m_runtimeId{RuntimeId}, m_system{wsl::windows::common::hcs::OpenComputeSystem(VmId.c_str(), GENERIC_ALL)}, m_shutdown{false}
27 -{
28 - m_devicesShutdown = false;
29 -}
30 -
31 -GUID DeviceHostProxy::AddNewDevice(const GUID& Type, const wil::com_ptr<IPlan9FileSystem>& Plan9Fs, const std::wstring& VirtIoTag)
32 -{
33 - const wrl::ComPtr<IUnknown> thisUnknown{CastToUnknown()};
34 - GUID instanceId{};
35 - THROW_IF_FAILED(UuidCreate(&instanceId));
36 - // Tell the device host to create the device.
37 - THROW_IF_FAILED(Plan9Fs->CreateVirtioDevice(m_systemId.c_str(), thisUnknown.Get(), VirtIoTag.c_str(), &instanceId));
38 -
39 - // Add the instance ID to the list of known devices. This must be done before the device is
40 - // added to the system, because doing that can cause the register doorbell function to be
41 - // called.
42 - // N.B. It will be removed if there is a failure.
43 - {
44 - auto lock = m_devicesLock.lock_exclusive();
45 - THROW_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
46 -
47 - m_devices.emplace(instanceId, DeviceHostProxyEntry{});
48 - }
49 -
50 - auto removeOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
51 - auto lock = m_devicesLock.lock_exclusive();
52 - m_devices.erase(instanceId);
53 - });
54 -
55 - // Add the device to the compute system on behalf of the device host.
56 - ModifySettingRequest<FlexibleIoDevice> request;
57 - request.RequestType = ModifyRequestType::Add;
58 - request.ResourcePath = L"VirtualMachine/Devices/FlexibleIov/";
59 - request.ResourcePath += wsl::shared::string::GuidToString<wchar_t>(instanceId, wsl::shared::string::GuidToStringFlags::None);
60 - request.Settings.EmulatorId = Type;
61 - request.Settings.HostingModel = FlexibleIoDeviceHostingModel::ExternalRestricted;
62 - wsl::windows::common::hcs::ModifyComputeSystem(m_system.get(), wsl::shared::ToJsonW(request).c_str());
63 - removeOnFailure.release();
64 - return instanceId;
65 -}
66 -
67 -void DeviceHostProxy::AddRemoteFileSystem(const GUID& ImplementationClsid, const std::wstring& Tag, const wil::com_ptr<IPlan9FileSystem>& Plan9Fs)
68 -{
69 - auto lock = m_lock.lock_exclusive();
70 - THROW_HR_IF(E_CHANGED_STATE, m_shutdown);
71 -
72 - // Make sure there are no duplicate tags.
73 - for (auto& entry : m_fileSystems)
74 - {
75 - THROW_HR_IF(E_INVALIDARG, entry.ImplementationClsid == ImplementationClsid && entry.Tag == Tag);
76 - }
77 -
78 - m_fileSystems.emplace_back(ImplementationClsid, Tag, Plan9Fs);
79 -}
80 -
81 -wil::com_ptr<IPlan9FileSystem> DeviceHostProxy::GetRemoteFileSystem(const GUID& ImplementationClsid, std::wstring_view Tag)
82 -{
83 - auto lock = m_lock.lock_shared();
84 - THROW_HR_IF(E_CHANGED_STATE, m_shutdown);
85 -
86 - for (auto& entry : m_fileSystems)
87 - {
88 - if (entry.ImplementationClsid == ImplementationClsid && entry.Tag == Tag)
89 - {
90 - return entry.Instance;
91 - }
92 - }
93 -
94 - return {};
95 -}
96 -
97 -void DeviceHostProxy::Shutdown()
98 -{
99 - {
100 - auto lock = m_lock.lock_exclusive();
101 - m_fileSystems.clear();
102 - m_shutdown = true;
103 - }
104 -
105 - {
106 - auto lock = m_devicesLock.lock_exclusive();
107 - m_devices.clear();
108 - m_devicesShutdown = true;
109 - }
110 -}
111 -
112 -HRESULT
113 -DeviceHostProxy::RegisterDeviceHost(_In_ IVmDeviceHost* DeviceHost, _In_ DWORD ProcessId, _Out_ UINT64* IpcSectionHandle)
114 -try
115 -{
116 - //
117 - // Because HdvProxyDeviceHost is not part of the API set, it is loaded here dynamically.
118 - //
119 -
120 - static LxssDynamicFunction<decltype(HdvProxyDeviceHost)> proxyDeviceHost{c_hdvModuleName, "HdvProxyDeviceHost"};
121 - const wil::com_ptr<IVmDeviceHost> remoteHost = DeviceHost;
122 - const wil::com_ptr<IUnknown> unknown = remoteHost.query<IUnknown>();
123 - THROW_IF_FAILED(proxyDeviceHost(m_system.get(), unknown.get(), ProcessId, IpcSectionHandle));
124 - return S_OK;
125 -}
126 -CATCH_RETURN()
127 -
128 -HRESULT
129 -DeviceHostProxy::NotifyAllDevicesInUse(_In_ LPCWSTR Tag)
130 -try
131 -{
132 - //
133 - // Add another Plan9 virtio device to the guest so additional mount commands will be possible.
134 - // This callback should be unused by virtiofs devices because a device is created for every
135 - // AddSharePath call.
136 - //
137 - auto p9fs = GetRemoteFileSystem(__uuidof(p9fs::Plan9FileSystem), Tag);
138 - THROW_HR_IF(E_NOT_SET, !p9fs);
139 - (void)AddNewDevice(VIRTIO_PLAN9_DEVICE_ID, p9fs, Tag);
140 - return S_OK;
141 -}
142 -CATCH_RETURN()
143 -
144 -HRESULT
145 -DeviceHostProxy::RegisterDoorbell(const GUID& InstanceId, UINT8 BarIndex, UINT64 Offset, UINT64 TriggerValue, UINT64 Flags, HANDLE Event)
146 -try
147 -{
148 - auto lock = m_devicesLock.lock_exclusive();
149 - RETURN_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
150 -
151 - // Check if the device is one of the known devices that doorbells can be registered for, and
152 - // if the device has not already registered a doorbell.
153 - // N.B. For security it is enforced that each device can only register a small number of doorbells.
154 - // Currently virtio-9p only uses one and the external virtio device uses two.
155 - const auto knownDevice = m_devices.find(InstanceId);
156 - RETURN_HR_IF(E_ACCESSDENIED, knownDevice == m_devices.end() || knownDevice->second.DoorbellCount == DEVICE_HOST_PROXY_DOORBELL_LIMIT);
157 -
158 - if (!knownDevice->second.MemoryNotification)
159 - {
160 - // Get an interface to the worker process to query devices.
161 - if (!m_deviceAccess)
162 - {
163 - static LxssDynamicFunction<GetVmWorkerProcessType<REFGUID, REFIID, IUnknown**>> getVmWorker{
164 - c_vmwpctrlModuleName, "GetVmWorkerProcess"};
165 -
166 - RETURN_IF_FAILED(getVmWorker(m_runtimeId, __uuidof(*m_deviceAccess), reinterpret_cast<IUnknown**>(&m_deviceAccess)));
167 - }
168 -
169 - RETURN_HR_IF(E_NOINTERFACE, !m_deviceAccess);
170 -
171 - // Retrieve the device's memory notification interface to register the doorbell, and store it
172 - // to be used during unregistration.
173 - wil::com_ptr<IUnknown> device;
174 - RETURN_IF_FAILED(m_deviceAccess->GetDevice(FLEXIO_DEVICE_ID, InstanceId, &device));
175 - knownDevice->second.MemoryNotification = device.query<IVmFiovGuestMemoryFastNotification>();
176 - }
177 -
178 - const auto result = knownDevice->second.MemoryNotification->RegisterDoorbell(
179 - static_cast<FIOV_BAR_SELECTOR>(BarIndex), Offset, TriggerValue, Flags, Event);
180 -
181 - if (SUCCEEDED(result))
182 - {
183 - ++knownDevice->second.DoorbellCount;
184 - }
185 -
186 - return result;
187 -}
188 -CATCH_RETURN()
189 -
190 -HRESULT
191 -DeviceHostProxy::UnregisterDoorbell(const GUID& InstanceId, UINT8 BarIndex, UINT64 Offset, UINT64 TriggerValue, UINT64 Flags)
192 -try
193 -{
194 - auto lock = m_devicesLock.lock_exclusive();
195 - RETURN_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
196 -
197 - // Check if the device is a known device and has registered a doorbell.
198 - // N.B. If the device is being removed, the device can't be retrieved from the worker process
199 - // so it's necessary to use the stored COM pointer.
200 - const auto device = m_devices.find(InstanceId);
201 - RETURN_HR_IF(E_ACCESSDENIED, device == m_devices.end() || device->second.DoorbellCount == 0);
202 - RETURN_IF_FAILED(device->second.MemoryNotification->UnregisterDoorbell(static_cast<FIOV_BAR_SELECTOR>(BarIndex), Offset, TriggerValue, Flags));
203 -
204 - if (--device->second.DoorbellCount == 0)
205 - {
206 - device->second.MemoryNotification.reset();
207 - }
208 -
209 - return S_OK;
210 -}
211 -CATCH_RETURN()
212 -
213 -HRESULT
214 -DeviceHostProxy::CreateSectionBackedMmioRange(
215 - const GUID& InstanceId, UINT8 BarIndex, UINT64 BarOffsetInPages, UINT64 PageCount, UINT64 MappingFlags, HANDLE SectionHandle, UINT64 SectionOffsetInPages)
216 -try
217 -{
218 - auto lock = m_devicesLock.lock_exclusive();
219 - RETURN_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
220 -
221 - // Check if the device is one of the known devices.
222 - const auto knownDevice = m_devices.find(InstanceId);
223 - THROW_HR_IF(E_ACCESSDENIED, knownDevice == m_devices.end());
224 -
225 - if (!knownDevice->second.MemoryMapping)
226 - {
227 - // Get an interface to the worker process to query devices.
228 - if (!m_deviceAccess)
229 - {
230 - static LxssDynamicFunction<GetVmWorkerProcessType<REFGUID, REFIID, IUnknown**>> getVmWorker{
231 - c_vmwpctrlModuleName, "GetVmWorkerProcess"};
232 - THROW_IF_FAILED(getVmWorker(m_runtimeId, __uuidof(*m_deviceAccess), reinterpret_cast<IUnknown**>(&m_deviceAccess)));
233 - }
234 -
235 - THROW_HR_IF(E_NOINTERFACE, !m_deviceAccess);
236 -
237 - // Retrieve the device specific interface to manage mapped sections.
238 - wil::com_ptr<IUnknown> device;
239 - THROW_IF_FAILED(m_deviceAccess->GetDevice(FLEXIO_DEVICE_ID, InstanceId, &device));
240 - knownDevice->second.MemoryMapping = device.query<IVmFiovGuestMmioMappings>();
241 - }
242 -
243 - THROW_IF_FAILED(knownDevice->second.MemoryMapping->CreateSectionBackedMmioRange(
244 - static_cast<FIOV_BAR_SELECTOR>(BarIndex), BarOffsetInPages, PageCount, static_cast<FiovMmioMappingFlags>(MappingFlags), SectionHandle, SectionOffsetInPages));
245 -
246 - return S_OK;
247 -}
248 -CATCH_RETURN()
249 -
250 -HRESULT
251 -DeviceHostProxy::DestroySectionBackedMmioRange(const GUID& InstanceId, UINT8 BarIndex, UINT64 BarOffsetInPages)
252 -try
253 -{
254 - auto lock = m_devicesLock.lock_exclusive();
255 - RETURN_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
256 - const auto device = m_devices.find(InstanceId);
257 - RETURN_HR_IF(E_ACCESSDENIED, device == m_devices.end() || !device->second.MemoryMapping);
258 - RETURN_IF_FAILED(device->second.MemoryMapping->DestroySectionBackedMmioRange(static_cast<FIOV_BAR_SELECTOR>(BarIndex), BarOffsetInPages));
259 - return S_OK;
260 -}
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#include "precomp.h"
4 +#include "DeviceHostProxy.h"
5 +
6 +// This template works around a limitation with decltype on overloaded functions. It will be able
7 +// to get the correct version of GetVmWorkerProcess based on the provided type arguments. By
8 +// doing it this way, a compiler error will be generated if someone changes the signature of
9 +// GetVmWorkerProcess.
10 +//
11 +// The way this works: decltype(GetVmWorkerProcess) does not work because it's overloaded.
12 +// decltype(GetVmWorkerProcess(arg1, ...)) works to select an overload if you have values of the
13 +// correct type (std::declval<T>() generates a value of the specified type), however the result
14 +// of that is the function's return type, not the function's type, so the argument types must
15 +// be repeated to reconstruct the function type.
16 +template <typename... Args>
17 +using GetVmWorkerProcessType = decltype(GetVmWorkerProcess(std::declval<Args>()...))(Args...);
18 +
19 +// Limit the number of allowed doorbells registered by an external HDV vdev. Currently virtio-9p only uses
20 +// one doorbell and wsldevicehost uses only two.
21 +#define DEVICE_HOST_PROXY_DOORBELL_LIMIT 8
22 +
23 +using namespace wsl::windows::common::hcs;
24 +
25 +DeviceHostProxy::DeviceHostProxy(const std::wstring& VmId, const GUID& RuntimeId) :
26 + m_systemId{VmId}, m_runtimeId{RuntimeId}, m_system{wsl::windows::common::hcs::OpenComputeSystem(VmId.c_str(), GENERIC_ALL)}, m_shutdown{false}
27 +{
28 + m_devicesShutdown = false;
29 + m_git = wil::CoCreateInstance<IGlobalInterfaceTable>(CLSID_StdGlobalInterfaceTable, CLSCTX_INPROC_SERVER);
30 +}
31 +
32 +GUID DeviceHostProxy::AddNewDevice(const GUID& Type, const wil::com_ptr<IPlan9FileSystem>& Plan9Fs, const std::wstring& VirtIoTag)
33 +{
34 + const wrl::ComPtr<IUnknown> thisUnknown{CastToUnknown()};
35 + GUID instanceId{};
36 + THROW_IF_FAILED(UuidCreate(&instanceId));
37 + // Tell the device host to create the device.
38 + THROW_IF_FAILED(Plan9Fs->CreateVirtioDevice(m_systemId.c_str(), thisUnknown.Get(), VirtIoTag.c_str(), &instanceId));
39 +
40 + // Add the instance ID to the list of known devices. This must be done before the device is
41 + // added to the system, because doing that can cause the register doorbell function to be
42 + // called.
43 + // N.B. It will be removed if there is a failure.
44 + {
45 + auto lock = m_devicesLock.lock_exclusive();
46 + THROW_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
47 +
48 + m_devices.emplace(instanceId, DeviceHostProxyEntry{});
49 + }
50 +
51 + auto removeOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
52 + auto lock = m_devicesLock.lock_exclusive();
53 + m_devices.erase(instanceId);
54 + });
55 +
56 + // Add the device to the compute system on behalf of the device host.
57 + ModifySettingRequest<FlexibleIoDevice> request;
58 + request.RequestType = ModifyRequestType::Add;
59 + request.ResourcePath = L"VirtualMachine/Devices/FlexibleIov/";
60 + request.ResourcePath += wsl::shared::string::GuidToString<wchar_t>(instanceId, wsl::shared::string::GuidToStringFlags::None);
61 + request.Settings.EmulatorId = Type;
62 + request.Settings.HostingModel = FlexibleIoDeviceHostingModel::ExternalRestricted;
63 + wsl::windows::common::hcs::ModifyComputeSystem(m_system.get(), wsl::shared::ToJsonW(request).c_str());
64 + removeOnFailure.release();
65 + return instanceId;
66 +}
67 +
68 +void DeviceHostProxy::RemoveDevice(const GUID& Type, const GUID& InstanceId)
69 +{
70 + {
71 + auto lock = m_devicesLock.lock_exclusive();
72 + THROW_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
73 + THROW_HR_IF(E_INVALIDARG, m_devices.find(InstanceId) == m_devices.end());
74 +
75 + m_devices.erase(InstanceId);
76 + }
77 +
78 + // N.B. Removing the FlexIov device is best effort since not all versions of Windows support it.
79 + try
80 + {
81 + ModifySettingRequest<FlexibleIoDevice> request;
82 + request.RequestType = ModifyRequestType::Remove;
83 + request.ResourcePath = L"VirtualMachine/Devices/FlexibleIov/";
84 + request.ResourcePath += wsl::shared::string::GuidToString<wchar_t>(InstanceId, wsl::shared::string::GuidToStringFlags::None);
85 + request.Settings.EmulatorId = Type;
86 + request.Settings.HostingModel = FlexibleIoDeviceHostingModel::ExternalRestricted;
87 + wsl::windows::common::hcs::ModifyComputeSystem(m_system.get(), wsl::shared::ToJsonW(request).c_str());
88 + }
89 + CATCH_LOG()
90 +}
91 +
92 +void DeviceHostProxy::AddRemoteFileSystem(const GUID& ImplementationClsid, const std::wstring& Tag, const wil::com_ptr<IPlan9FileSystem>& Plan9Fs)
93 +{
94 + auto lock = m_lock.lock_exclusive();
95 + THROW_HR_IF(E_CHANGED_STATE, m_shutdown);
96 +
97 + // Make sure there are no duplicate tags.
98 + for (auto& entry : m_fileSystems)
99 + {
100 + THROW_HR_IF(E_INVALIDARG, entry.ImplementationClsid == ImplementationClsid && entry.Tag == Tag);
101 + }
102 +
103 + m_fileSystems.emplace_back(ImplementationClsid, Tag, Plan9Fs, m_git.get());
104 +}
105 +
106 +wil::com_ptr<IPlan9FileSystem> DeviceHostProxy::GetRemoteFileSystem(const GUID& ImplementationClsid, std::wstring_view Tag)
107 +{
108 + auto lock = m_lock.lock_shared();
109 + THROW_HR_IF(E_CHANGED_STATE, m_shutdown);
110 +
111 + for (auto& entry : m_fileSystems)
112 + {
113 + if (entry.ImplementationClsid == ImplementationClsid && entry.Tag == Tag)
114 + {
115 + // Retrieve the instance from the global interface table to ensure the correct apartment/thread affinity.
116 + // This is required because we might be running under MTA or NA depending on which class we were called from.
117 +
118 + wil::com_ptr<IPlan9FileSystem> instance;
119 + THROW_IF_FAILED(
120 + m_git->GetInterfaceFromGlobal(entry.Cookie, __uuidof(IPlan9FileSystem), reinterpret_cast<void**>(instance.put())));
121 + return instance;
122 + }
123 + }
124 +
125 + return {};
126 +}
127 +
128 +void DeviceHostProxy::Shutdown()
129 +{
130 + {
131 + auto lock = m_lock.lock_exclusive();
132 + m_fileSystems.clear();
133 + m_shutdown = true;
134 + }
135 +
136 + {
137 + auto lock = m_devicesLock.lock_exclusive();
138 + m_devices.clear();
139 + m_devicesShutdown = true;
140 + }
141 +}
142 +
143 +HRESULT
144 +DeviceHostProxy::RegisterDeviceHost(_In_ IVmDeviceHost* DeviceHost, _In_ DWORD ProcessId, _Out_ UINT64* IpcSectionHandle)
145 +try
146 +{
147 + //
148 + // Because HdvProxyDeviceHost is not part of the API set, it is loaded here dynamically.
149 + //
150 +
151 + static LxssDynamicFunction<decltype(HdvProxyDeviceHost)> proxyDeviceHost{c_hdvModuleName, "HdvProxyDeviceHost"};
152 + const wil::com_ptr<IVmDeviceHost> remoteHost = DeviceHost;
153 + const wil::com_ptr<IUnknown> unknown = remoteHost.query<IUnknown>();
154 + THROW_IF_FAILED(proxyDeviceHost(m_system.get(), unknown.get(), ProcessId, IpcSectionHandle));
155 + return S_OK;
156 +}
157 +CATCH_RETURN()
158 +
159 +HRESULT
160 +DeviceHostProxy::NotifyAllDevicesInUse(_In_ LPCWSTR Tag)
161 +try
162 +{
163 + //
164 + // Add another Plan9 virtio device to the guest so additional mount commands will be possible.
165 + // This callback should be unused by virtiofs devices because a device is created for every
166 + // AddSharePath call.
167 + //
168 + auto p9fs = GetRemoteFileSystem(__uuidof(p9fs::Plan9FileSystem), Tag);
169 + THROW_HR_IF(E_NOT_SET, !p9fs);
170 + (void)AddNewDevice(VIRTIO_PLAN9_DEVICE_ID, p9fs, Tag);
171 + return S_OK;
172 +}
173 +CATCH_RETURN()
174 +
175 +HRESULT
176 +DeviceHostProxy::RegisterDoorbell(const GUID& InstanceId, UINT8 BarIndex, UINT64 Offset, UINT64 TriggerValue, UINT64 Flags, HANDLE Event)
177 +try
178 +{
179 + auto lock = m_devicesLock.lock_exclusive();
180 + RETURN_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
181 +
182 + // Check if the device is one of the known devices that doorbells can be registered for, and
183 + // if the device has not already registered a doorbell.
184 + // N.B. For security it is enforced that each device can only register a small number of doorbells.
185 + // Currently virtio-9p only uses one and the external virtio device uses two.
186 + const auto knownDevice = m_devices.find(InstanceId);
187 + RETURN_HR_IF(E_ACCESSDENIED, knownDevice == m_devices.end() || knownDevice->second.DoorbellCount == DEVICE_HOST_PROXY_DOORBELL_LIMIT);
188 +
189 + if (!knownDevice->second.MemoryNotification)
190 + {
191 + // Get an interface to the worker process to query devices.
192 + if (!m_deviceAccess)
193 + {
194 + static LxssDynamicFunction<GetVmWorkerProcessType<REFGUID, REFIID, IUnknown**>> getVmWorker{
195 + c_vmwpctrlModuleName, "GetVmWorkerProcess"};
196 +
197 + RETURN_IF_FAILED(getVmWorker(m_runtimeId, __uuidof(*m_deviceAccess), reinterpret_cast<IUnknown**>(&m_deviceAccess)));
198 + }
199 +
200 + RETURN_HR_IF(E_NOINTERFACE, !m_deviceAccess);
201 +
202 + // Retrieve the device's memory notification interface to register the doorbell, and store it
203 + // to be used during unregistration.
204 + wil::com_ptr<IUnknown> device;
205 + RETURN_IF_FAILED(m_deviceAccess->GetDevice(FLEXIO_DEVICE_ID, InstanceId, &device));
206 + knownDevice->second.MemoryNotification = device.query<IVmFiovGuestMemoryFastNotification>();
207 + }
208 +
209 + const auto result = knownDevice->second.MemoryNotification->RegisterDoorbell(
210 + static_cast<FIOV_BAR_SELECTOR>(BarIndex), Offset, TriggerValue, Flags, Event);
211 +
212 + if (SUCCEEDED(result))
213 + {
214 + ++knownDevice->second.DoorbellCount;
215 + }
216 +
217 + return result;
218 +}
219 +CATCH_RETURN()
220 +
221 +HRESULT
222 +DeviceHostProxy::UnregisterDoorbell(const GUID& InstanceId, UINT8 BarIndex, UINT64 Offset, UINT64 TriggerValue, UINT64 Flags)
223 +try
224 +{
225 + auto lock = m_devicesLock.lock_exclusive();
226 + RETURN_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
227 +
228 + // Check if the device is a known device and has registered a doorbell.
229 + // N.B. If the device is being removed, the device can't be retrieved from the worker process
230 + // so it's necessary to use the stored COM pointer.
231 + const auto device = m_devices.find(InstanceId);
232 + RETURN_HR_IF(E_ACCESSDENIED, device == m_devices.end() || device->second.DoorbellCount == 0);
233 + RETURN_IF_FAILED(device->second.MemoryNotification->UnregisterDoorbell(static_cast<FIOV_BAR_SELECTOR>(BarIndex), Offset, TriggerValue, Flags));
234 +
235 + if (--device->second.DoorbellCount == 0)
236 + {
237 + device->second.MemoryNotification.reset();
238 + }
239 +
240 + return S_OK;
241 +}
242 +CATCH_RETURN()
243 +
244 +HRESULT
245 +DeviceHostProxy::CreateSectionBackedMmioRange(
246 + const GUID& InstanceId, UINT8 BarIndex, UINT64 BarOffsetInPages, UINT64 PageCount, UINT64 MappingFlags, HANDLE SectionHandle, UINT64 SectionOffsetInPages)
247 +try
248 +{
249 + auto lock = m_devicesLock.lock_exclusive();
250 + RETURN_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
251 +
252 + // Check if the device is one of the known devices.
253 + const auto knownDevice = m_devices.find(InstanceId);
254 + THROW_HR_IF(E_ACCESSDENIED, knownDevice == m_devices.end());
255 +
256 + if (!knownDevice->second.MemoryMapping)
257 + {
258 + // Get an interface to the worker process to query devices.
259 + if (!m_deviceAccess)
260 + {
261 + static LxssDynamicFunction<GetVmWorkerProcessType<REFGUID, REFIID, IUnknown**>> getVmWorker{
262 + c_vmwpctrlModuleName, "GetVmWorkerProcess"};
263 + THROW_IF_FAILED(getVmWorker(m_runtimeId, __uuidof(*m_deviceAccess), reinterpret_cast<IUnknown**>(&m_deviceAccess)));
264 + }
265 +
266 + THROW_HR_IF(E_NOINTERFACE, !m_deviceAccess);
267 +
268 + // Retrieve the device specific interface to manage mapped sections.
269 + wil::com_ptr<IUnknown> device;
270 + THROW_IF_FAILED(m_deviceAccess->GetDevice(FLEXIO_DEVICE_ID, InstanceId, &device));
271 + knownDevice->second.MemoryMapping = device.query<IVmFiovGuestMmioMappings>();
272 + }
273 +
274 + THROW_IF_FAILED(knownDevice->second.MemoryMapping->CreateSectionBackedMmioRange(
275 + static_cast<FIOV_BAR_SELECTOR>(BarIndex), BarOffsetInPages, PageCount, static_cast<FiovMmioMappingFlags>(MappingFlags), SectionHandle, SectionOffsetInPages));
276 +
277 + return S_OK;
278 +}
279 +CATCH_RETURN()
280 +
281 +HRESULT
282 +DeviceHostProxy::DestroySectionBackedMmioRange(const GUID& InstanceId, UINT8 BarIndex, UINT64 BarOffsetInPages)
283 +try
284 +{
285 + auto lock = m_devicesLock.lock_exclusive();
286 + RETURN_HR_IF(E_CHANGED_STATE, m_devicesShutdown);
287 + const auto device = m_devices.find(InstanceId);
288 + RETURN_HR_IF(E_ACCESSDENIED, device == m_devices.end() || !device->second.MemoryMapping);
289 + RETURN_IF_FAILED(device->second.MemoryMapping->DestroySectionBackedMmioRange(static_cast<FIOV_BAR_SELECTOR>(BarIndex), BarOffsetInPages));
290 + return S_OK;
291 +}
292 CATCH_RETURN()
\ No newline at end of file
src/windows/common/DeviceHostProxy.h
+118 -75
@@ -1,76 +1,119 @@
1 -// Copyright (C) Microsoft Corporation. All rights reserved.
2 -
3 -#pragma once
4 -
5 -#include <windowsdefs.h>
6 -#include "hcs.hpp"
7 -
8 -namespace wrl = Microsoft::WRL;
9 -
10 -class DeviceHostProxy : public wrl::RuntimeClass<wrl::RuntimeClassFlags<wrl::RuntimeClassType::ClassicCom>, IVmDeviceHostSupport, IPlan9FileSystemHost>
11 -{
12 -public:
13 - DeviceHostProxy(const std::wstring& VmId, const GUID& RuntimeId);
14 -
15 - GUID AddNewDevice(const GUID& Type, const wil::com_ptr<IPlan9FileSystem>& Plan9Fs, const std::wstring& VirtIoTag);
16 -
17 - void AddRemoteFileSystem(const GUID& ImplementationClsid, const std::wstring& Tag, const wil::com_ptr<IPlan9FileSystem>& Plan9Fs);
18 -
19 - wil::com_ptr<IPlan9FileSystem> GetRemoteFileSystem(const GUID& ImplementationClsid, std::wstring_view Tag);
20 -
21 - void Shutdown();
22 -
23 - //
24 - // IVmDeviceHostSupport
25 - //
26 - IFACEMETHOD(RegisterDeviceHost)(_In_ IVmDeviceHost* DeviceHost, _In_ DWORD ProcessId, _Out_ UINT64* IpcSectionHandle) override;
27 -
28 - //
29 - // IPlan9FileSystemHost
30 - //
31 - IFACEMETHOD(NotifyAllDevicesInUse)(_In_ LPCWSTR Tag) override;
32 -
33 - IFACEMETHOD(RegisterDoorbell)(const GUID& InstanceId, UINT8 BarIndex, UINT64 Offset, UINT64 TriggerValue, UINT64 Flags, HANDLE Event) override;
34 -
35 - IFACEMETHOD(UnregisterDoorbell)(const GUID& InstanceId, UINT8 BarIndex, UINT64 Offset, UINT64 TriggerValue, UINT64 Flags) override;
36 -
37 - IFACEMETHOD(CreateSectionBackedMmioRange)(
38 - const GUID& InstanceId, UINT8 BarIndex, UINT64 BarOffsetInPages, UINT64 PageCount, UINT64 MappingFlags, HANDLE SectionHandle, UINT64 SectionOffsetInPages) override;
39 -
40 - IFACEMETHOD(DestroySectionBackedMmioRange)(const GUID& InstanceId, UINT8 BarIndex, UINT64 BarOffsetInPages) override;
41 -
42 -private:
43 - struct RemoteFileSystemInfo
44 - {
45 - RemoteFileSystemInfo(GUID ImplementationClsid, const std::wstring& Tag, const wil::com_ptr<IPlan9FileSystem>& Instance) :
46 - ImplementationClsid{ImplementationClsid}, Tag{Tag}, Instance{Instance}
47 - {
48 - }
49 -
50 - GUID ImplementationClsid;
51 - std::wstring Tag;
52 - wil::com_ptr<IPlan9FileSystem> Instance;
53 - };
54 -
55 - std::wstring m_systemId;
56 - GUID m_runtimeId;
57 - wsl::windows::common::hcs::unique_hcs_system m_system;
58 - wil::srwlock m_lock;
59 - std::vector<RemoteFileSystemInfo> m_fileSystems;
60 - bool m_shutdown;
61 -
62 - struct DeviceHostProxyEntry
63 - {
64 - wil::com_ptr<IVmFiovGuestMemoryFastNotification> MemoryNotification;
65 - wil::com_ptr<IVmFiovGuestMmioMappings> MemoryMapping;
66 - size_t DoorbellCount = 0;
67 - };
68 -
69 - wil::com_ptr<IVmVirtualDeviceAccess> m_deviceAccess;
70 - wil::srwlock m_devicesLock;
71 - std::map<GUID, DeviceHostProxyEntry, wsl::windows::common::helpers::GuidLess> m_devices;
72 - bool m_devicesShutdown;
73 -
74 - static constexpr LPCWSTR c_hdvModuleName = L"vmdevicehost.dll";
75 - static constexpr LPCWSTR c_vmwpctrlModuleName = L"vmwpctrl.dll";
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#pragma once
4 +
5 +#include <windowsdefs.h>
6 +#include "hcs.hpp"
7 +
8 +namespace wrl = Microsoft::WRL;
9 +
10 +class DeviceHostProxy : public wrl::RuntimeClass<wrl::RuntimeClassFlags<wrl::RuntimeClassType::ClassicCom>, IVmDeviceHostSupport, IPlan9FileSystemHost>
11 +{
12 +public:
13 + DeviceHostProxy(const std::wstring& VmId, const GUID& RuntimeId);
14 +
15 + GUID AddNewDevice(const GUID& Type, const wil::com_ptr<IPlan9FileSystem>& Plan9Fs, const std::wstring& VirtIoTag);
16 +
17 + void RemoveDevice(const GUID& Type, const GUID& InstanceId);
18 +
19 + void AddRemoteFileSystem(const GUID& ImplementationClsid, const std::wstring& Tag, const wil::com_ptr<IPlan9FileSystem>& Plan9Fs);
20 +
21 + wil::com_ptr<IPlan9FileSystem> GetRemoteFileSystem(const GUID& ImplementationClsid, std::wstring_view Tag);
22 +
23 + void Shutdown();
24 +
25 + //
26 + // IVmDeviceHostSupport
27 + //
28 + IFACEMETHOD(RegisterDeviceHost)(_In_ IVmDeviceHost* DeviceHost, _In_ DWORD ProcessId, _Out_ UINT64* IpcSectionHandle) override;
29 +
30 + //
31 + // IPlan9FileSystemHost
32 + //
33 + IFACEMETHOD(NotifyAllDevicesInUse)(_In_ LPCWSTR Tag) override;
34 +
35 + IFACEMETHOD(RegisterDoorbell)(const GUID& InstanceId, UINT8 BarIndex, UINT64 Offset, UINT64 TriggerValue, UINT64 Flags, HANDLE Event) override;
36 +
37 + IFACEMETHOD(UnregisterDoorbell)(const GUID& InstanceId, UINT8 BarIndex, UINT64 Offset, UINT64 TriggerValue, UINT64 Flags) override;
38 +
39 + IFACEMETHOD(CreateSectionBackedMmioRange)(
40 + const GUID& InstanceId, UINT8 BarIndex, UINT64 BarOffsetInPages, UINT64 PageCount, UINT64 MappingFlags, HANDLE SectionHandle, UINT64 SectionOffsetInPages) override;
41 +
42 + IFACEMETHOD(DestroySectionBackedMmioRange)(const GUID& InstanceId, UINT8 BarIndex, UINT64 BarOffsetInPages) override;
43 +
44 +private:
45 + struct RemoteFileSystemInfo
46 + {
47 + RemoteFileSystemInfo(GUID ImplementationClsid, const std::wstring& Tag, const wil::com_ptr<IPlan9FileSystem>& Instance, IGlobalInterfaceTable* git) :
48 + ImplementationClsid{ImplementationClsid}, Tag{Tag}, m_git{git}
49 + {
50 + THROW_IF_FAILED(git->RegisterInterfaceInGlobal(Instance.get(), __uuidof(IPlan9FileSystem), &Cookie));
51 + }
52 +
53 + ~RemoteFileSystemInfo()
54 + {
55 + if (Cookie != 0)
56 + {
57 + LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(Cookie));
58 + }
59 + }
60 +
61 + RemoteFileSystemInfo(RemoteFileSystemInfo&& other) noexcept
62 + {
63 + *this = std::move(other);
64 + }
65 +
66 + RemoteFileSystemInfo& operator=(RemoteFileSystemInfo&& other) noexcept
67 + {
68 + if (this != &other)
69 + {
70 + if (Cookie != 0)
71 + {
72 + LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(Cookie));
73 + }
74 +
75 + ImplementationClsid = other.ImplementationClsid;
76 + Tag = std::move(other.Tag);
77 + Cookie = other.Cookie;
78 + m_git = other.m_git;
79 + other.Cookie = 0;
80 + }
81 +
82 + return *this;
83 + }
84 +
85 + RemoteFileSystemInfo(const RemoteFileSystemInfo&) = delete;
86 + RemoteFileSystemInfo& operator=(const RemoteFileSystemInfo&) = delete;
87 +
88 + GUID ImplementationClsid{};
89 + std::wstring Tag;
90 + DWORD Cookie = 0;
91 +
92 + private:
93 + IGlobalInterfaceTable* m_git = nullptr;
94 + };
95 +
96 + wil::com_ptr<IGlobalInterfaceTable> m_git;
97 +
98 + std::wstring m_systemId;
99 + GUID m_runtimeId;
100 + wsl::windows::common::hcs::unique_hcs_system m_system;
101 + wil::srwlock m_lock;
102 + std::vector<RemoteFileSystemInfo> m_fileSystems;
103 + bool m_shutdown;
104 +
105 + struct DeviceHostProxyEntry
106 + {
107 + wil::com_ptr<IVmFiovGuestMemoryFastNotification> MemoryNotification;
108 + wil::com_ptr<IVmFiovGuestMmioMappings> MemoryMapping;
109 + size_t DoorbellCount = 0;
110 + };
111 +
112 + wil::com_ptr<IVmVirtualDeviceAccess> m_deviceAccess;
113 + wil::srwlock m_devicesLock;
114 + std::map<GUID, DeviceHostProxyEntry, wsl::windows::common::helpers::GuidLess> m_devices;
115 + bool m_devicesShutdown;
116 +
117 + static constexpr LPCWSTR c_hdvModuleName = L"vmdevicehost.dll";
118 + static constexpr LPCWSTR c_vmwpctrlModuleName = L"vmwpctrl.dll";
119 };
\ No newline at end of file
src/windows/common/Dmesg.cpp
+31 -6
@@ -15,8 +15,13 @@ Abstract:
15 #include "precomp.h"
16 #include "Dmesg.h"
17
18 -DmesgCollector::DmesgCollector(GUID VmId, const wil::unique_event& ExitEvent, bool EnableTelemetry, bool EnableDebugConsole, const std::wstring& Com1PipeName) :
19 - m_com1PipeName(Com1PipeName), m_runtimeId(VmId), m_debugConsole(EnableDebugConsole), m_telemetry(EnableTelemetry)
18 +DmesgCollector::DmesgCollector(
19 + GUID VmId, const wil::unique_event& ExitEvent, bool EnableTelemetry, bool EnableDebugConsole, const std::wstring& Com1PipeName, wil::unique_handle&& OutputHandle) :
20 + m_com1PipeName(Com1PipeName),
21 + m_runtimeId(VmId),
22 + m_debugConsole(EnableDebugConsole),
23 + m_telemetry(EnableTelemetry),
24 + m_outputHandle(std::move(OutputHandle))
25 {
26 m_exitEvent.reset(wsl::windows::common::wslutil::DuplicateHandle(ExitEvent.get()));
27 m_overlappedEvent.create(wil::EventOptions::ManualReset);
@@ -40,10 +45,16 @@ DmesgCollector::~DmesgCollector()
45 }
46
47 std::shared_ptr<DmesgCollector> DmesgCollector::Create(
43 - GUID VmId, const wil::unique_event& ExitEvent, bool EnableTelemetry, bool EnableDebugConsole, const std::wstring& Com1PipeName, bool EnableEarlyBootConsole)
48 + GUID VmId,
49 + const wil::unique_event& ExitEvent,
50 + bool EnableTelemetry,
51 + bool EnableDebugConsole,
52 + const std::wstring& Com1PipeName,
53 + bool EnableEarlyBootConsole,
54 + wil::unique_handle&& OutputHandle)
55 {
45 - auto dmesgCollector =
46 - std::shared_ptr<DmesgCollector>(new DmesgCollector(VmId, ExitEvent, EnableTelemetry, EnableDebugConsole, Com1PipeName));
56 + auto dmesgCollector = std::shared_ptr<DmesgCollector>(
57 + new DmesgCollector(VmId, ExitEvent, EnableTelemetry, EnableDebugConsole, Com1PipeName, std::move(OutputHandle)));
58
59 if (FAILED(dmesgCollector->Start(EnableEarlyBootConsole)))
60 {
@@ -89,7 +100,11 @@ std::pair<std::wstring, std::thread> DmesgCollector::StartDmesgThread(InputSourc
100 Self->ProcessInput(Source, validBuffer);
101 }
102 }
92 - CATCH_LOG()
103 + catch (...)
104 + {
105 + auto error = wil::ResultFromCaughtException();
106 + LOG_HR_IF(error, error != E_ABORT); // E_ABORT is expected during shutdown.
107 + }
108 });
109
110 return std::pair{std::move(pipeName), std::move(workerThread)};
@@ -142,6 +157,16 @@ void DmesgCollector::ProcessInput(InputSource Source, const gsl::span<char>& Inp
157 {
158 WriteToCom1(Input);
159 }
160 +
161 + if (m_outputHandle != nullptr)
162 + {
163 + m_overlappedEvent.ResetEvent();
164 + if (wsl::windows::common::relay::InterruptableWrite(
165 + m_outputHandle.get(), gslhelpers::convert_span<gsl::byte>(Input), m_exitEvents, &m_overlapped) == 0)
166 + {
167 + m_outputHandle = nullptr;
168 + }
169 + }
170 }
171
172 void DmesgCollector::WriteToCom1(const gsl::span<char>& Input)
src/windows/common/Dmesg.h
+15 -2
@@ -34,7 +34,13 @@ public:
34 }
35
36 static std::shared_ptr<DmesgCollector> Create(
37 - GUID VmId, const wil::unique_event& ExitEvent, bool EnableTelemetry, bool EnableDebugConsole, const std::wstring& Com1PipeName, bool EnableEarlyBootConsole);
37 + GUID VmId,
38 + const wil::unique_event& ExitEvent,
39 + bool EnableTelemetry,
40 + bool EnableDebugConsole,
41 + const std::wstring& Com1PipeName,
42 + bool EnableEarlyBootConsole,
43 + wil::unique_handle&& OutputHandle);
44
45 private:
46 enum InputSource
@@ -43,7 +49,13 @@ private:
49 DmesgCollectorConsole
50 };
51
46 - DmesgCollector(GUID VmId, const wil::unique_event& ExitEvent, bool EnableTelemetry, bool EnableDebugConsole, const std::wstring& Com1PipeName);
52 + DmesgCollector(
53 + GUID VmId,
54 + const wil::unique_event& ExitEvent,
55 + bool EnableTelemetry,
56 + bool EnableDebugConsole,
57 + const std::wstring& Com1PipeName,
58 + wil::unique_handle&& OutputHandle = {});
59
60 HRESULT Start(bool EnableEarlyBootConsole);
61 std::pair<std::wstring, std::thread> StartDmesgThread(InputSource Source);
@@ -70,4 +82,5 @@ private:
82 bool m_waitForConnection;
83 std::thread m_earlyConsoleWorker;
84 std::thread m_virtioWorker;
85 + wil::unique_handle m_outputHandle = nullptr;
86 };
src/windows/common/DnsResolver.cpp
+417 -417
@@ -1,417 +1,417 @@
1 -// Copyright (C) Microsoft Corporation. All rights reserved.
2 -
3 -#include <LxssDynamicFunction.h>
4 -#include "precomp.h"
5 -#include "DnsResolver.h"
6 -
7 -using wsl::core::networking::DnsResolver;
8 -
9 -static constexpr auto c_dnsModuleName = L"dnsapi.dll";
10 -
11 -std::optional<LxssDynamicFunction<decltype(DnsQueryRaw)>> DnsResolver::s_dnsQueryRaw;
12 -std::optional<LxssDynamicFunction<decltype(DnsCancelQueryRaw)>> DnsResolver::s_dnsCancelQueryRaw;
13 -std::optional<LxssDynamicFunction<decltype(DnsQueryRawResultFree)>> DnsResolver::s_dnsQueryRawResultFree;
14 -
15 -HRESULT DnsResolver::LoadDnsResolverMethods() noexcept
16 -{
17 - static wil::shared_hmodule dnsModule;
18 - static DWORD loadError = ERROR_SUCCESS;
19 - static std::once_flag dnsLoadFlag;
20 -
21 - // Load DNS dll only once
22 - std::call_once(dnsLoadFlag, [&]() {
23 - dnsModule.reset(LoadLibraryEx(c_dnsModuleName, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32));
24 - if (!dnsModule)
25 - {
26 - loadError = GetLastError();
27 - }
28 - });
29 -
30 - RETURN_IF_WIN32_ERROR_MSG(loadError, "LoadLibraryEx %ls", c_dnsModuleName);
31 -
32 - // Initialize dynamic functions for the DNS tunneling Windows APIs.
33 - // using the non-throwing instance of LxssDynamicFunction as to not end up in the Error telemetry
34 - LxssDynamicFunction<decltype(DnsQueryRaw)> local_dnsQueryRaw{DynamicFunctionErrorLogs::None};
35 - RETURN_IF_FAILED_EXPECTED(local_dnsQueryRaw.load(dnsModule, "DnsQueryRaw"));
36 - LxssDynamicFunction<decltype(DnsCancelQueryRaw)> local_dnsCancelQueryRaw{DynamicFunctionErrorLogs::None};
37 - RETURN_IF_FAILED_EXPECTED(local_dnsCancelQueryRaw.load(dnsModule, "DnsCancelQueryRaw"));
38 - LxssDynamicFunction<decltype(DnsQueryRawResultFree)> local_dnsQueryRawResultFree{DynamicFunctionErrorLogs::None};
39 - RETURN_IF_FAILED_EXPECTED(local_dnsQueryRawResultFree.load(dnsModule, "DnsQueryRawResultFree"));
40 -
41 - // Make a dummy call to the DNS APIs to verify if they are working. The APIs are going to be present
42 - // on older Windows versions, where they can be turned on/off. If turned off, the APIs
43 - // will be unusable and will return ERROR_CALL_NOT_IMPLEMENTED.
44 - if (local_dnsQueryRaw(nullptr, nullptr) == ERROR_CALL_NOT_IMPLEMENTED)
45 - {
46 - RETURN_IF_WIN32_ERROR_EXPECTED(ERROR_CALL_NOT_IMPLEMENTED);
47 - }
48 -
49 - s_dnsQueryRaw.emplace(std::move(local_dnsQueryRaw));
50 - s_dnsCancelQueryRaw.emplace(std::move(local_dnsCancelQueryRaw));
51 - s_dnsQueryRawResultFree.emplace(std::move(local_dnsQueryRawResultFree));
52 - return S_OK;
53 -}
54 -
55 -DnsResolver::DnsResolver(wil::unique_socket&& dnsHvsocket, DnsResolverFlags flags) :
56 - m_dnsChannel(
57 - std::move(dnsHvsocket),
58 - [this](const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) {
59 - ProcessDnsRequest(dnsBuffer, dnsClientIdentifier);
60 - }),
61 - m_flags(flags)
62 -{
63 - // Initialize as signaled, as there are no requests yet
64 - m_allRequestsFinished.SetEvent();
65 -
66 - // Read external interface constraint regkey
67 - const auto lxssKey = windows::common::registry::OpenLxssMachineKey(KEY_READ);
68 - m_externalInterfaceConstraintName =
69 - windows::common::registry::ReadString(lxssKey.get(), nullptr, c_interfaceConstraintKey, L"");
70 -
71 - if (!m_externalInterfaceConstraintName.empty())
72 - {
73 - ResolveExternalInterfaceConstraintIndex();
74 -
75 - WSL_LOG(
76 - "DnsResolver::DnsResolver",
77 - TraceLoggingValue(m_externalInterfaceConstraintName.c_str(), "m_externalInterfaceConstraintName"),
78 - TraceLoggingValue(m_externalInterfaceConstraintIndex, "m_externalInterfaceConstraintIndex"));
79 -
80 - // Register for interface change notifications. Notifications are used to determine if the external interface constraint setting is applicable.
81 - THROW_IF_WIN32_ERROR(NotifyIpInterfaceChange(AF_UNSPEC, &DnsResolver::InterfaceChangeCallback, this, FALSE, &m_interfaceNotificationHandle));
82 - }
83 -}
84 -
85 -DnsResolver::~DnsResolver() noexcept
86 -{
87 - Stop();
88 -}
89 -
90 -void DnsResolver::GenerateTelemetry() noexcept
91 -try
92 -{
93 - // Find the 3 most common DNS API failures
94 - uint32_t mostCommonDnsStatusError = 0;
95 - uint32_t mostCommonDnsStatusErrorCount = 0;
96 - uint32_t secondCommonDnsStatusError = 0;
97 - uint32_t secondCommonDnsStatusErrorCount = 0;
98 - uint32_t thirdCommonDnsStatusError = 0;
99 - uint32_t thirdCommonDnsStatusErrorCount = 0;
100 -
101 - std::vector<std::pair<uint32_t, uint32_t>> failures(m_dnsApiFailures.size());
102 - std::copy(m_dnsApiFailures.begin(), m_dnsApiFailures.end(), failures.begin());
103 -
104 - // Sort in descending order based on failure count
105 - std::sort(failures.begin(), failures.end(), [](const auto& lhs, const auto& rhs) { return lhs.second > rhs.second; });
106 -
107 - if (failures.size() >= 1)
108 - {
109 - mostCommonDnsStatusError = failures[0].first;
110 - mostCommonDnsStatusErrorCount = failures[0].second;
111 - }
112 - if (failures.size() >= 2)
113 - {
114 - secondCommonDnsStatusError = failures[1].first;
115 - secondCommonDnsStatusErrorCount = failures[1].second;
116 - }
117 - if (failures.size() >= 3)
118 - {
119 - thirdCommonDnsStatusError = failures[2].first;
120 - thirdCommonDnsStatusErrorCount = failures[2].second;
121 - }
122 -
123 - // Add telemetry with DNS tunneling statistics, before shutting down
124 - WSL_LOG(
125 - "DnsTunnelingStatistics",
126 - TraceLoggingValue(m_totalUdpQueries.load(), "totalUdpQueries"),
127 - TraceLoggingValue(m_successfulUdpQueries.load(), "successfulUdpQueries"),
128 - TraceLoggingValue(m_totalTcpQueries.load(), "totalTcpQueries"),
129 - TraceLoggingValue(m_successfulTcpQueries.load(), "successfulTcpQueries"),
130 - TraceLoggingValue(m_queriesWithNullResult.load(), "queriesWithNullResult"),
131 - TraceLoggingValue(m_failedDnsQueryRawCalls.load(), "FailedDnsQueryRawCalls"),
132 - TraceLoggingValue(m_dnsApiFailures.size(), "totalDnsStatusErrorInstances"),
133 - TraceLoggingValue(mostCommonDnsStatusError, "mostCommonDnsStatusError"),
134 - TraceLoggingValue(mostCommonDnsStatusErrorCount, "mostCommonDnsStatusErrorCount"),
135 - TraceLoggingValue(secondCommonDnsStatusError, "secondCommonDnsStatusError"),
136 - TraceLoggingValue(secondCommonDnsStatusErrorCount, "secondCommonDnsStatusErrorCount"),
137 - TraceLoggingValue(thirdCommonDnsStatusError, "thirdCommonDnsStatusError"),
138 - TraceLoggingValue(thirdCommonDnsStatusErrorCount, "thirdCommonDnsStatusErrorCount"));
139 -}
140 -CATCH_LOG()
141 -
142 -void DnsResolver::Stop() noexcept
143 -try
144 -{
145 - WSL_LOG("DnsResolver::Stop");
146 -
147 - // Scoped m_dnsLock
148 - {
149 - const std::lock_guard lock(m_dnsLock);
150 -
151 - m_stopped = true;
152 -
153 - // Cancel existing requests. Cancel is complete when DnsQueryRawCallback is
154 - // invoked with status == ERROR_CANCELLED
155 - // N.B. Cancelling can end up calling the DnsQueryRawCallback directly on this same thread. i.e., while this
156 - // lock is held. Which is fine because m_dnsLock is a recursive mutex.
157 - // N.B. Cancelling a query will synchronously remove the query from m_dnsRequests, which invalidates iterators.
158 -
159 - std::vector<DNS_QUERY_RAW_CANCEL*> cancelHandles;
160 - cancelHandles.reserve(m_dnsRequests.size());
161 -
162 - for (auto& [_, context] : m_dnsRequests)
163 - {
164 - cancelHandles.emplace_back(&context->m_cancelHandle);
165 - }
166 -
167 - for (const auto e : cancelHandles)
168 - {
169 - LOG_IF_WIN32_ERROR(s_dnsCancelQueryRaw.value()(e));
170 - }
171 - }
172 -
173 - // Wait for all requests to complete. At this point no new requests can be started since the object is stopped.
174 - // We are only waiting for existing requests to finish.
175 - m_allRequestsFinished.wait();
176 -
177 - // Stop the response queue first as it can make calls in m_dnsChannel
178 - m_dnsResponseQueue.cancel();
179 -
180 - m_dnsChannel.Stop();
181 -
182 - // Stop interface change notifications
183 - m_interfaceNotificationHandle.reset();
184 -
185 - GenerateTelemetry();
186 -}
187 -CATCH_LOG()
188 -
189 -void DnsResolver::ProcessDnsRequest(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept
190 -try
191 -{
192 - const std::lock_guard lock(m_dnsLock);
193 - if (m_stopped)
194 - {
195 - return;
196 - }
197 -
198 - WSL_LOG_DEBUG(
199 - "DnsResolver::ProcessDnsRequest - received new DNS request",
200 - TraceLoggingValue(dnsBuffer.size(), "DNS buffer size"),
201 - TraceLoggingValue(dnsClientIdentifier.Protocol == IPPROTO_UDP ? "UDP" : "TCP", "Protocol"),
202 - TraceLoggingValue(dnsClientIdentifier.DnsClientId, "DNS client id"),
203 - TraceLoggingValue(!m_externalInterfaceConstraintName.empty(), "Is ExternalInterfaceConstraint configured"),
204 - TraceLoggingValue(m_externalInterfaceConstraintIndex, "m_externalInterfaceConstraintIndex"));
205 -
206 - // If the external interface constraint is configured but it is *not* present/up, WSL should be net-blind, so we avoid making DNS requests.
207 - if (!m_externalInterfaceConstraintName.empty() && m_externalInterfaceConstraintIndex == 0)
208 - {
209 - return;
210 - }
211 -
212 - dnsClientIdentifier.Protocol == IPPROTO_UDP ? m_totalUdpQueries++ : m_totalTcpQueries++;
213 -
214 - // Get next request id. If value reaches UINT_MAX + 1 it will be automatically reset to 0
215 - const auto requestId = m_currentRequestId++;
216 -
217 - // Create the DNS request context
218 - auto context = std::make_unique<DnsResolver::DnsQueryContext>(
219 - requestId, dnsClientIdentifier, [this](_Inout_ DnsResolver::DnsQueryContext* context, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) {
220 - HandleDnsQueryCompletion(context, queryResults);
221 - });
222 -
223 - auto [it, _] = m_dnsRequests.emplace(requestId, std::move(context));
224 - const auto localContext = it->second.get();
225 -
226 - auto removeContextOnError = wil::scope_exit([&] { WI_VERIFY(m_dnsRequests.erase(requestId) == 1); });
227 -
228 - // Fill DNS request structure
229 - DNS_QUERY_RAW_REQUEST request{};
230 -
231 - request.version = DNS_QUERY_RAW_REQUEST_VERSION1;
232 - request.resultsVersion = DNS_QUERY_RAW_RESULTS_VERSION1;
233 - request.dnsQueryRawSize = static_cast<ULONG>(dnsBuffer.size());
234 - request.dnsQueryRaw = (PBYTE)dnsBuffer.data();
235 - request.protocol = (dnsClientIdentifier.Protocol == IPPROTO_TCP) ? DNS_PROTOCOL_TCP : DNS_PROTOCOL_UDP;
236 - request.queryCompletionCallback = DnsResolver::DnsQueryRawCallback;
237 - request.queryContext = localContext;
238 - // Only unicast UDP & TCP queries are tunneled. Pass this flag to tell Windows DNS client to *not* resolve using multicast.
239 - request.queryOptions |= DNS_QUERY_NO_MULTICAST;
240 -
241 - // In a DNS request from Linux there might be DNS records that Windows DNS client does not know how to parse.
242 - // By default in this case Windows will fail the request. When the flag is enabled, Windows will extract the
243 - // question from the DNS request and attempt to resolve it, ignoring the unknown records.
244 - if (WI_IsFlagSet(m_flags, DnsResolverFlags::BestEffortDnsParsing))
245 - {
246 - request.queryRawOptions |= DNS_QUERY_RAW_OPTION_BEST_EFFORT_PARSE;
247 - }
248 -
249 - // If the external interface constraint is configured and present on the host, only send DNS requests on that interface.
250 - if (m_externalInterfaceConstraintIndex != 0)
251 - {
252 - request.interfaceIndex = m_externalInterfaceConstraintIndex;
253 - }
254 -
255 - // Start the DNS request
256 - // N.B. All DNS requests will bypass the Windows DNS cache
257 - const auto result = s_dnsQueryRaw.value()(&request, &localContext->m_cancelHandle);
258 - if (result != DNS_REQUEST_PENDING)
259 - {
260 - m_failedDnsQueryRawCalls++;
261 -
262 - WSL_LOG(
263 - "ProcessDnsRequestFailed",
264 - TraceLoggingValue(requestId, "requestId"),
265 - TraceLoggingValue(result, "result"),
266 - TraceLoggingValue("DnsQueryRaw", "executionStep"));
267 - return;
268 - }
269 -
270 - removeContextOnError.release();
271 -
272 - m_allRequestsFinished.ResetEvent();
273 -}
274 -CATCH_LOG()
275 -
276 -void DnsResolver::HandleDnsQueryCompletion(_Inout_ DnsResolver::DnsQueryContext* queryContext, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) noexcept
277 -try
278 -{
279 - // Always free the query result structure
280 - const auto freeQueryResults = wil::scope_exit([&] {
281 - if (queryResults != nullptr)
282 - {
283 - s_dnsQueryRawResultFree.value()(queryResults);
284 - }
285 - });
286 -
287 - const std::lock_guard lock(m_dnsLock);
288 -
289 - if (queryResults != nullptr)
290 - {
291 - WSL_LOG(
292 - "DnsResolver::HandleDnsQueryCompletion",
293 - TraceLoggingValue(queryContext->m_id, "queryContext->m_id"),
294 - TraceLoggingValue(queryResults->queryStatus, "queryResults->queryStatus"),
295 - TraceLoggingValue(queryResults->queryRawResponse != nullptr, "validResponse"));
296 -
297 - // Note: The response may be valid even if queryResults->queryStatus is not 0, for example when the DNS server returns a negative response.
298 - if (queryResults->queryRawResponse != nullptr)
299 - {
300 - queryContext->m_dnsClientIdentifier.Protocol == IPPROTO_UDP ? m_successfulUdpQueries++ : m_successfulTcpQueries++;
301 - }
302 - // the Windows DNS API returned failure
303 - else
304 - {
305 - if (m_dnsApiFailures.find(queryResults->queryStatus) == m_dnsApiFailures.end())
306 - {
307 - m_dnsApiFailures[queryResults->queryStatus] = 1;
308 - }
309 - else
310 - {
311 - m_dnsApiFailures[queryResults->queryStatus]++;
312 - }
313 - }
314 - }
315 - else
316 - {
317 - WSL_LOG(
318 - "DnsResolver::HandleDnsQueryCompletion - received a NULL queryResults",
319 - TraceLoggingValue(queryContext->m_id, "queryContext->m_id"));
320 - m_queriesWithNullResult++;
321 - }
322 -
323 - if (!m_stopped && queryResults != nullptr && queryResults->queryRawResponse != nullptr)
324 - {
325 - // Copy DNS response buffer
326 - std::vector<gsl::byte> dnsResponse(queryResults->queryRawResponseSize);
327 - CopyMemory(dnsResponse.data(), queryResults->queryRawResponse, queryResults->queryRawResponseSize);
328 -
329 - WSL_LOG_DEBUG(
330 - "DnsResolver::HandleDnsQueryCompletion - received new DNS response",
331 - TraceLoggingValue(dnsResponse.size(), "DNS buffer size"),
332 - TraceLoggingValue(queryContext->m_dnsClientIdentifier.Protocol == IPPROTO_UDP ? "UDP" : "TCP", "Protocol"),
333 - TraceLoggingValue(queryContext->m_dnsClientIdentifier.DnsClientId, "DNS client id"));
334 -
335 - // Schedule the DNS response to be sent to Linux
336 - m_dnsResponseQueue.submit([this, dnsResponse = std::move(dnsResponse), dnsClientIdentifier = queryContext->m_dnsClientIdentifier]() mutable {
337 - m_dnsChannel.SendDnsMessage(gsl::make_span(dnsResponse), dnsClientIdentifier);
338 - });
339 - }
340 -
341 - // Stop tracking this DNS request and delete the request context
342 - WI_VERIFY(m_dnsRequests.erase(queryContext->m_id) == 1);
343 -
344 - // Set event if all tracked requests have finished
345 - if (m_dnsRequests.empty())
346 - {
347 - m_allRequestsFinished.SetEvent();
348 - }
349 -}
350 -CATCH_LOG()
351 -
352 -void DnsResolver::ResolveExternalInterfaceConstraintIndex() noexcept
353 -try
354 -{
355 - const std::lock_guard lock(m_dnsLock);
356 - if (m_stopped)
357 - {
358 - return;
359 - }
360 -
361 - if (m_externalInterfaceConstraintName.empty())
362 - {
363 - return;
364 - }
365 -
366 - NET_LUID interfaceLuid{};
367 - ULONG interfaceIndex = 0;
368 -
369 - // Update the interface index on every exit path.
370 - // The calls below to convert interface name to index will fail if the interface does not exist anymore,
371 - // in which case we still need to reset the interface index to its default value of 0.
372 - const auto setInterfaceIndex = wil::scope_exit([&] {
373 - if (interfaceIndex != m_externalInterfaceConstraintIndex)
374 - {
375 - WSL_LOG(
376 - "DnsResolver::ResolveExternalInterfaceConstraintIndex - setting m_externalInterfaceConstraintIndex to new value",
377 - TraceLoggingValue(m_externalInterfaceConstraintIndex, "old interface index"),
378 - TraceLoggingValue(interfaceIndex, "new interface index"));
379 -
380 - m_externalInterfaceConstraintIndex = interfaceIndex;
381 - }
382 - });
383 -
384 - // If external interface constraint is configured, query to see if it's present on the host.
385 - auto errorCode = ConvertInterfaceAliasToLuid(m_externalInterfaceConstraintName.c_str(), &interfaceLuid);
386 - if (FAILED_WIN32_LOG(errorCode))
387 - {
388 - return;
389 - }
390 -
391 - errorCode = ConvertInterfaceLuidToIndex(&interfaceLuid, reinterpret_cast<PNET_IFINDEX>(&interfaceIndex));
392 - if (FAILED_WIN32_LOG(errorCode))
393 - {
394 - return;
395 - }
396 -}
397 -CATCH_LOG()
398 -
399 -VOID CALLBACK DnsResolver::DnsQueryRawCallback(_In_ VOID* queryContext, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) noexcept
400 -try
401 -{
402 - assert(queryContext != nullptr);
403 -
404 - const auto context = static_cast<DnsQueryContext*>(queryContext);
405 -
406 - // Call into DnsResolver parent object to process the query result
407 - context->m_handleQueryCompletion(context, queryResults);
408 -}
409 -CATCH_LOG()
410 -
411 -VOID CALLBACK DnsResolver::InterfaceChangeCallback(_In_ PVOID context, PMIB_IPINTERFACE_ROW, MIB_NOTIFICATION_TYPE) noexcept
412 -try
413 -{
414 - const auto dnsResolver = static_cast<DnsResolver*>(context);
415 - dnsResolver->ResolveExternalInterfaceConstraintIndex();
416 -}
417 -CATCH_LOG()
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#include <LxssDynamicFunction.h>
4 +#include "precomp.h"
5 +#include "DnsResolver.h"
6 +
7 +using wsl::core::networking::DnsResolver;
8 +
9 +static constexpr auto c_dnsModuleName = L"dnsapi.dll";
10 +
11 +std::optional<LxssDynamicFunction<decltype(DnsQueryRaw)>> DnsResolver::s_dnsQueryRaw;
12 +std::optional<LxssDynamicFunction<decltype(DnsCancelQueryRaw)>> DnsResolver::s_dnsCancelQueryRaw;
13 +std::optional<LxssDynamicFunction<decltype(DnsQueryRawResultFree)>> DnsResolver::s_dnsQueryRawResultFree;
14 +
15 +HRESULT DnsResolver::LoadDnsResolverMethods() noexcept
16 +{
17 + static wil::shared_hmodule dnsModule;
18 + static DWORD loadError = ERROR_SUCCESS;
19 + static std::once_flag dnsLoadFlag;
20 +
21 + // Load DNS dll only once
22 + std::call_once(dnsLoadFlag, [&]() {
23 + dnsModule.reset(LoadLibraryEx(c_dnsModuleName, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32));
24 + if (!dnsModule)
25 + {
26 + loadError = GetLastError();
27 + }
28 + });
29 +
30 + RETURN_IF_WIN32_ERROR_MSG(loadError, "LoadLibraryEx %ls", c_dnsModuleName);
31 +
32 + // Initialize dynamic functions for the DNS tunneling Windows APIs.
33 + // using the non-throwing instance of LxssDynamicFunction as to not end up in the Error telemetry
34 + LxssDynamicFunction<decltype(DnsQueryRaw)> local_dnsQueryRaw{DynamicFunctionErrorLogs::None};
35 + RETURN_IF_FAILED_EXPECTED(local_dnsQueryRaw.load(dnsModule, "DnsQueryRaw"));
36 + LxssDynamicFunction<decltype(DnsCancelQueryRaw)> local_dnsCancelQueryRaw{DynamicFunctionErrorLogs::None};
37 + RETURN_IF_FAILED_EXPECTED(local_dnsCancelQueryRaw.load(dnsModule, "DnsCancelQueryRaw"));
38 + LxssDynamicFunction<decltype(DnsQueryRawResultFree)> local_dnsQueryRawResultFree{DynamicFunctionErrorLogs::None};
39 + RETURN_IF_FAILED_EXPECTED(local_dnsQueryRawResultFree.load(dnsModule, "DnsQueryRawResultFree"));
40 +
41 + // Make a dummy call to the DNS APIs to verify if they are working. The APIs are going to be present
42 + // on older Windows versions, where they can be turned on/off. If turned off, the APIs
43 + // will be unusable and will return ERROR_CALL_NOT_IMPLEMENTED.
44 + if (local_dnsQueryRaw(nullptr, nullptr) == ERROR_CALL_NOT_IMPLEMENTED)
45 + {
46 + RETURN_IF_WIN32_ERROR_EXPECTED(ERROR_CALL_NOT_IMPLEMENTED);
47 + }
48 +
49 + s_dnsQueryRaw.emplace(std::move(local_dnsQueryRaw));
50 + s_dnsCancelQueryRaw.emplace(std::move(local_dnsCancelQueryRaw));
51 + s_dnsQueryRawResultFree.emplace(std::move(local_dnsQueryRawResultFree));
52 + return S_OK;
53 +}
54 +
55 +DnsResolver::DnsResolver(wil::unique_socket&& dnsHvsocket, DnsResolverFlags flags) :
56 + m_dnsChannel(
57 + std::move(dnsHvsocket),
58 + [this](const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) {
59 + ProcessDnsRequest(dnsBuffer, dnsClientIdentifier);
60 + }),
61 + m_flags(flags)
62 +{
63 + // Initialize as signaled, as there are no requests yet
64 + m_allRequestsFinished.SetEvent();
65 +
66 + // Read external interface constraint regkey
67 + const auto lxssKey = windows::common::registry::OpenLxssMachineKey(KEY_READ);
68 + m_externalInterfaceConstraintName =
69 + windows::common::registry::ReadString(lxssKey.get(), nullptr, c_interfaceConstraintKey, L"");
70 +
71 + if (!m_externalInterfaceConstraintName.empty())
72 + {
73 + ResolveExternalInterfaceConstraintIndex();
74 +
75 + WSL_LOG(
76 + "DnsResolver::DnsResolver",
77 + TraceLoggingValue(m_externalInterfaceConstraintName.c_str(), "m_externalInterfaceConstraintName"),
78 + TraceLoggingValue(m_externalInterfaceConstraintIndex, "m_externalInterfaceConstraintIndex"));
79 +
80 + // Register for interface change notifications. Notifications are used to determine if the external interface constraint setting is applicable.
81 + THROW_IF_WIN32_ERROR(NotifyIpInterfaceChange(AF_UNSPEC, &DnsResolver::InterfaceChangeCallback, this, FALSE, &m_interfaceNotificationHandle));
82 + }
83 +}
84 +
85 +DnsResolver::~DnsResolver() noexcept
86 +{
87 + Stop();
88 +}
89 +
90 +void DnsResolver::GenerateTelemetry() noexcept
91 +try
92 +{
93 + // Find the 3 most common DNS API failures
94 + uint32_t mostCommonDnsStatusError = 0;
95 + uint32_t mostCommonDnsStatusErrorCount = 0;
96 + uint32_t secondCommonDnsStatusError = 0;
97 + uint32_t secondCommonDnsStatusErrorCount = 0;
98 + uint32_t thirdCommonDnsStatusError = 0;
99 + uint32_t thirdCommonDnsStatusErrorCount = 0;
100 +
101 + std::vector<std::pair<uint32_t, uint32_t>> failures(m_dnsApiFailures.size());
102 + std::copy(m_dnsApiFailures.begin(), m_dnsApiFailures.end(), failures.begin());
103 +
104 + // Sort in descending order based on failure count
105 + std::sort(failures.begin(), failures.end(), [](const auto& lhs, const auto& rhs) { return lhs.second > rhs.second; });
106 +
107 + if (failures.size() >= 1)
108 + {
109 + mostCommonDnsStatusError = failures[0].first;
110 + mostCommonDnsStatusErrorCount = failures[0].second;
111 + }
112 + if (failures.size() >= 2)
113 + {
114 + secondCommonDnsStatusError = failures[1].first;
115 + secondCommonDnsStatusErrorCount = failures[1].second;
116 + }
117 + if (failures.size() >= 3)
118 + {
119 + thirdCommonDnsStatusError = failures[2].first;
120 + thirdCommonDnsStatusErrorCount = failures[2].second;
121 + }
122 +
123 + // Add telemetry with DNS tunneling statistics, before shutting down
124 + WSL_LOG(
125 + "DnsTunnelingStatistics",
126 + TraceLoggingValue(m_totalUdpQueries.load(), "totalUdpQueries"),
127 + TraceLoggingValue(m_successfulUdpQueries.load(), "successfulUdpQueries"),
128 + TraceLoggingValue(m_totalTcpQueries.load(), "totalTcpQueries"),
129 + TraceLoggingValue(m_successfulTcpQueries.load(), "successfulTcpQueries"),
130 + TraceLoggingValue(m_queriesWithNullResult.load(), "queriesWithNullResult"),
131 + TraceLoggingValue(m_failedDnsQueryRawCalls.load(), "FailedDnsQueryRawCalls"),
132 + TraceLoggingValue(m_dnsApiFailures.size(), "totalDnsStatusErrorInstances"),
133 + TraceLoggingValue(mostCommonDnsStatusError, "mostCommonDnsStatusError"),
134 + TraceLoggingValue(mostCommonDnsStatusErrorCount, "mostCommonDnsStatusErrorCount"),
135 + TraceLoggingValue(secondCommonDnsStatusError, "secondCommonDnsStatusError"),
136 + TraceLoggingValue(secondCommonDnsStatusErrorCount, "secondCommonDnsStatusErrorCount"),
137 + TraceLoggingValue(thirdCommonDnsStatusError, "thirdCommonDnsStatusError"),
138 + TraceLoggingValue(thirdCommonDnsStatusErrorCount, "thirdCommonDnsStatusErrorCount"));
139 +}
140 +CATCH_LOG()
141 +
142 +void DnsResolver::Stop() noexcept
143 +try
144 +{
145 + WSL_LOG("DnsResolver::Stop");
146 +
147 + // Scoped m_dnsLock
148 + {
149 + const std::lock_guard lock(m_dnsLock);
150 +
151 + m_stopped = true;
152 +
153 + // Cancel existing requests. Cancel is complete when DnsQueryRawCallback is
154 + // invoked with status == ERROR_CANCELLED
155 + // N.B. Cancelling can end up calling the DnsQueryRawCallback directly on this same thread. i.e., while this
156 + // lock is held. Which is fine because m_dnsLock is a recursive mutex.
157 + // N.B. Cancelling a query will synchronously remove the query from m_dnsRequests, which invalidates iterators.
158 +
159 + std::vector<DNS_QUERY_RAW_CANCEL*> cancelHandles;
160 + cancelHandles.reserve(m_dnsRequests.size());
161 +
162 + for (auto& [_, context] : m_dnsRequests)
163 + {
164 + cancelHandles.emplace_back(&context->m_cancelHandle);
165 + }
166 +
167 + for (const auto e : cancelHandles)
168 + {
169 + LOG_IF_WIN32_ERROR(s_dnsCancelQueryRaw.value()(e));
170 + }
171 + }
172 +
173 + // Wait for all requests to complete. At this point no new requests can be started since the object is stopped.
174 + // We are only waiting for existing requests to finish.
175 + m_allRequestsFinished.wait();
176 +
177 + // Stop the response queue first as it can make calls in m_dnsChannel
178 + m_dnsResponseQueue.cancel();
179 +
180 + m_dnsChannel.Stop();
181 +
182 + // Stop interface change notifications
183 + m_interfaceNotificationHandle.reset();
184 +
185 + GenerateTelemetry();
186 +}
187 +CATCH_LOG()
188 +
189 +void DnsResolver::ProcessDnsRequest(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept
190 +try
191 +{
192 + const std::lock_guard lock(m_dnsLock);
193 + if (m_stopped)
194 + {
195 + return;
196 + }
197 +
198 + WSL_LOG_DEBUG(
199 + "DnsResolver::ProcessDnsRequest - received new DNS request",
200 + TraceLoggingValue(dnsBuffer.size(), "DNS buffer size"),
201 + TraceLoggingValue(dnsClientIdentifier.Protocol == IPPROTO_UDP ? "UDP" : "TCP", "Protocol"),
202 + TraceLoggingValue(dnsClientIdentifier.DnsClientId, "DNS client id"),
203 + TraceLoggingValue(!m_externalInterfaceConstraintName.empty(), "Is ExternalInterfaceConstraint configured"),
204 + TraceLoggingValue(m_externalInterfaceConstraintIndex, "m_externalInterfaceConstraintIndex"));
205 +
206 + // If the external interface constraint is configured but it is *not* present/up, WSL should be net-blind, so we avoid making DNS requests.
207 + if (!m_externalInterfaceConstraintName.empty() && m_externalInterfaceConstraintIndex == 0)
208 + {
209 + return;
210 + }
211 +
212 + dnsClientIdentifier.Protocol == IPPROTO_UDP ? m_totalUdpQueries++ : m_totalTcpQueries++;
213 +
214 + // Get next request id. If value reaches UINT_MAX + 1 it will be automatically reset to 0
215 + const auto requestId = m_currentRequestId++;
216 +
217 + // Create the DNS request context
218 + auto context = std::make_unique<DnsResolver::DnsQueryContext>(
219 + requestId, dnsClientIdentifier, [this](_Inout_ DnsResolver::DnsQueryContext* context, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) {
220 + HandleDnsQueryCompletion(context, queryResults);
221 + });
222 +
223 + auto [it, _] = m_dnsRequests.emplace(requestId, std::move(context));
224 + const auto localContext = it->second.get();
225 +
226 + auto removeContextOnError = wil::scope_exit([&] { WI_VERIFY(m_dnsRequests.erase(requestId) == 1); });
227 +
228 + // Fill DNS request structure
229 + DNS_QUERY_RAW_REQUEST request{};
230 +
231 + request.version = DNS_QUERY_RAW_REQUEST_VERSION1;
232 + request.resultsVersion = DNS_QUERY_RAW_RESULTS_VERSION1;
233 + request.dnsQueryRawSize = static_cast<ULONG>(dnsBuffer.size());
234 + request.dnsQueryRaw = (PBYTE)dnsBuffer.data();
235 + request.protocol = (dnsClientIdentifier.Protocol == IPPROTO_TCP) ? DNS_PROTOCOL_TCP : DNS_PROTOCOL_UDP;
236 + request.queryCompletionCallback = DnsResolver::DnsQueryRawCallback;
237 + request.queryContext = localContext;
238 + // Only unicast UDP & TCP queries are tunneled. Pass this flag to tell Windows DNS client to *not* resolve using multicast.
239 + request.queryOptions |= DNS_QUERY_NO_MULTICAST;
240 +
241 + // In a DNS request from Linux there might be DNS records that Windows DNS client does not know how to parse.
242 + // By default in this case Windows will fail the request. When the flag is enabled, Windows will extract the
243 + // question from the DNS request and attempt to resolve it, ignoring the unknown records.
244 + if (WI_IsFlagSet(m_flags, DnsResolverFlags::BestEffortDnsParsing))
245 + {
246 + request.queryRawOptions |= DNS_QUERY_RAW_OPTION_BEST_EFFORT_PARSE;
247 + }
248 +
249 + // If the external interface constraint is configured and present on the host, only send DNS requests on that interface.
250 + if (m_externalInterfaceConstraintIndex != 0)
251 + {
252 + request.interfaceIndex = m_externalInterfaceConstraintIndex;
253 + }
254 +
255 + // Start the DNS request
256 + // N.B. All DNS requests will bypass the Windows DNS cache
257 + const auto result = s_dnsQueryRaw.value()(&request, &localContext->m_cancelHandle);
258 + if (result != DNS_REQUEST_PENDING)
259 + {
260 + m_failedDnsQueryRawCalls++;
261 +
262 + WSL_LOG(
263 + "ProcessDnsRequestFailed",
264 + TraceLoggingValue(requestId, "requestId"),
265 + TraceLoggingValue(result, "result"),
266 + TraceLoggingValue("DnsQueryRaw", "executionStep"));
267 + return;
268 + }
269 +
270 + removeContextOnError.release();
271 +
272 + m_allRequestsFinished.ResetEvent();
273 +}
274 +CATCH_LOG()
275 +
276 +void DnsResolver::HandleDnsQueryCompletion(_Inout_ DnsResolver::DnsQueryContext* queryContext, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) noexcept
277 +try
278 +{
279 + // Always free the query result structure
280 + const auto freeQueryResults = wil::scope_exit([&] {
281 + if (queryResults != nullptr)
282 + {
283 + s_dnsQueryRawResultFree.value()(queryResults);
284 + }
285 + });
286 +
287 + const std::lock_guard lock(m_dnsLock);
288 +
289 + if (queryResults != nullptr)
290 + {
291 + WSL_LOG(
292 + "DnsResolver::HandleDnsQueryCompletion",
293 + TraceLoggingValue(queryContext->m_id, "queryContext->m_id"),
294 + TraceLoggingValue(queryResults->queryStatus, "queryResults->queryStatus"),
295 + TraceLoggingValue(queryResults->queryRawResponse != nullptr, "validResponse"));
296 +
297 + // Note: The response may be valid even if queryResults->queryStatus is not 0, for example when the DNS server returns a negative response.
298 + if (queryResults->queryRawResponse != nullptr)
299 + {
300 + queryContext->m_dnsClientIdentifier.Protocol == IPPROTO_UDP ? m_successfulUdpQueries++ : m_successfulTcpQueries++;
301 + }
302 + // the Windows DNS API returned failure
303 + else
304 + {
305 + if (m_dnsApiFailures.find(queryResults->queryStatus) == m_dnsApiFailures.end())
306 + {
307 + m_dnsApiFailures[queryResults->queryStatus] = 1;
308 + }
309 + else
310 + {
311 + m_dnsApiFailures[queryResults->queryStatus]++;
312 + }
313 + }
314 + }
315 + else
316 + {
317 + WSL_LOG(
318 + "DnsResolver::HandleDnsQueryCompletion - received a NULL queryResults",
319 + TraceLoggingValue(queryContext->m_id, "queryContext->m_id"));
320 + m_queriesWithNullResult++;
321 + }
322 +
323 + if (!m_stopped && queryResults != nullptr && queryResults->queryRawResponse != nullptr)
324 + {
325 + // Copy DNS response buffer
326 + std::vector<gsl::byte> dnsResponse(queryResults->queryRawResponseSize);
327 + CopyMemory(dnsResponse.data(), queryResults->queryRawResponse, queryResults->queryRawResponseSize);
328 +
329 + WSL_LOG_DEBUG(
330 + "DnsResolver::HandleDnsQueryCompletion - received new DNS response",
331 + TraceLoggingValue(dnsResponse.size(), "DNS buffer size"),
332 + TraceLoggingValue(queryContext->m_dnsClientIdentifier.Protocol == IPPROTO_UDP ? "UDP" : "TCP", "Protocol"),
333 + TraceLoggingValue(queryContext->m_dnsClientIdentifier.DnsClientId, "DNS client id"));
334 +
335 + // Schedule the DNS response to be sent to Linux
336 + m_dnsResponseQueue.submit([this, dnsResponse = std::move(dnsResponse), dnsClientIdentifier = queryContext->m_dnsClientIdentifier]() mutable {
337 + m_dnsChannel.SendDnsMessage(gsl::make_span(dnsResponse), dnsClientIdentifier);
338 + });
339 + }
340 +
341 + // Stop tracking this DNS request and delete the request context
342 + WI_VERIFY(m_dnsRequests.erase(queryContext->m_id) == 1);
343 +
344 + // Set event if all tracked requests have finished
345 + if (m_dnsRequests.empty())
346 + {
347 + m_allRequestsFinished.SetEvent();
348 + }
349 +}
350 +CATCH_LOG()
351 +
352 +void DnsResolver::ResolveExternalInterfaceConstraintIndex() noexcept
353 +try
354 +{
355 + const std::lock_guard lock(m_dnsLock);
356 + if (m_stopped)
357 + {
358 + return;
359 + }
360 +
361 + if (m_externalInterfaceConstraintName.empty())
362 + {
363 + return;
364 + }
365 +
366 + NET_LUID interfaceLuid{};
367 + ULONG interfaceIndex = 0;
368 +
369 + // Update the interface index on every exit path.
370 + // The calls below to convert interface name to index will fail if the interface does not exist anymore,
371 + // in which case we still need to reset the interface index to its default value of 0.
372 + const auto setInterfaceIndex = wil::scope_exit([&] {
373 + if (interfaceIndex != m_externalInterfaceConstraintIndex)
374 + {
375 + WSL_LOG(
376 + "DnsResolver::ResolveExternalInterfaceConstraintIndex - setting m_externalInterfaceConstraintIndex to new value",
377 + TraceLoggingValue(m_externalInterfaceConstraintIndex, "old interface index"),
378 + TraceLoggingValue(interfaceIndex, "new interface index"));
379 +
380 + m_externalInterfaceConstraintIndex = interfaceIndex;
381 + }
382 + });
383 +
384 + // If external interface constraint is configured, query to see if it's present on the host.
385 + auto errorCode = ConvertInterfaceAliasToLuid(m_externalInterfaceConstraintName.c_str(), &interfaceLuid);
386 + if (FAILED_WIN32_LOG(errorCode))
387 + {
388 + return;
389 + }
390 +
391 + errorCode = ConvertInterfaceLuidToIndex(&interfaceLuid, reinterpret_cast<PNET_IFINDEX>(&interfaceIndex));
392 + if (FAILED_WIN32_LOG(errorCode))
393 + {
394 + return;
395 + }
396 +}
397 +CATCH_LOG()
398 +
399 +VOID CALLBACK DnsResolver::DnsQueryRawCallback(_In_ VOID* queryContext, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) noexcept
400 +try
401 +{
402 + assert(queryContext != nullptr);
403 +
404 + const auto context = static_cast<DnsQueryContext*>(queryContext);
405 +
406 + // Call into DnsResolver parent object to process the query result
407 + context->m_handleQueryCompletion(context, queryResults);
408 +}
409 +CATCH_LOG()
410 +
411 +VOID CALLBACK DnsResolver::InterfaceChangeCallback(_In_ PVOID context, PMIB_IPINTERFACE_ROW, MIB_NOTIFICATION_TYPE) noexcept
412 +try
413 +{
414 + const auto dnsResolver = static_cast<DnsResolver*>(context);
415 + dnsResolver->ResolveExternalInterfaceConstraintIndex();
416 +}
417 +CATCH_LOG()
src/windows/common/DnsResolver.h
+141 -141
@@ -1,141 +1,141 @@
1 -// Copyright (C) Microsoft Corporation. All rights reserved.
2 -
3 -#pragma once
4 -
5 -#include "DnsTunnelingChannel.h"
6 -#include "WslCoreMessageQueue.h"
7 -#include "WslCoreNetworkingSupport.h"
8 -
9 -namespace wsl::core::networking {
10 -
11 -enum class DnsResolverFlags
12 -{
13 - None = 0x0,
14 - BestEffortDnsParsing = 0x1
15 -};
16 -DEFINE_ENUM_FLAG_OPERATORS(DnsResolverFlags);
17 -
18 -class DnsResolver
19 -{
20 -public:
21 - DnsResolver(wil::unique_socket&& dnsHvsocket, DnsResolverFlags flags);
22 - ~DnsResolver() noexcept;
23 -
24 - DnsResolver(const DnsResolver&) = delete;
25 - DnsResolver& operator=(const DnsResolver&) = delete;
26 -
27 - DnsResolver(DnsResolver&&) = delete;
28 - DnsResolver& operator=(DnsResolver&&) = delete;
29 -
30 - void Stop() noexcept;
31 -
32 - static HRESULT LoadDnsResolverMethods() noexcept;
33 -
34 -private:
35 - struct DnsQueryContext
36 - {
37 - // Struct containing protocol (TCP/UDP) and unique id of the Linux DNS client making the request.
38 - LX_GNS_DNS_CLIENT_IDENTIFIER m_dnsClientIdentifier{};
39 -
40 - // Handle used to cancel the request.
41 - DNS_QUERY_RAW_CANCEL m_cancelHandle{};
42 -
43 - // Unique query id.
44 - uint32_t m_id{};
45 -
46 - // Callback to the parent object to notify about the DNS query completion.
47 - std::function<void(DnsQueryContext*, DNS_QUERY_RAW_RESULT*)> m_handleQueryCompletion;
48 -
49 - DnsQueryContext(
50 - uint32_t id,
51 - const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier,
52 - std::function<void(DnsQueryContext*, DNS_QUERY_RAW_RESULT*)>&& handleQueryCompletion) :
53 - m_dnsClientIdentifier(dnsClientIdentifier), m_id(id), m_handleQueryCompletion(std::move(handleQueryCompletion))
54 - {
55 - }
56 -
57 - ~DnsQueryContext() noexcept = default;
58 -
59 - DnsQueryContext(const DnsQueryContext&) = delete;
60 - DnsQueryContext& operator=(const DnsQueryContext&) = delete;
61 - DnsQueryContext(DnsQueryContext&&) = delete;
62 - DnsQueryContext& operator=(DnsQueryContext&&) = delete;
63 - };
64 -
65 - void GenerateTelemetry() noexcept;
66 -
67 - // Process DNS request received from Linux.
68 - //
69 - // Arguments:
70 - // dnsBuffer - buffer containing DNS request.
71 - // dnsClientIdentifier - struct containing protocol (TCP/UDP) and unique id of the Linux DNS client making the request.
72 - void ProcessDnsRequest(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept;
73 -
74 - // Handle completion of DNS query.
75 - //
76 - // Arguments:
77 - // dnsQueryContext - context structure for the DNS request.
78 - // queryResults - structure containing result of the DNS request.
79 - void HandleDnsQueryCompletion(_Inout_ DnsQueryContext* dnsQueryContext, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) noexcept;
80 -
81 - void ResolveExternalInterfaceConstraintIndex() noexcept;
82 -
83 - // Callback that will be invoked by the DNS API whenever a request finishes. The callback is invoked on success, error or when request is cancelled.
84 - //
85 - // Arguments:
86 - // queryContext - pointer to context structure, will be a structure of type DnsQueryContext.
87 - // queryResults - pointer to structure containing the result of the DNS request.
88 - static VOID CALLBACK DnsQueryRawCallback(_In_ VOID* queryContext, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) noexcept;
89 -
90 - static VOID CALLBACK InterfaceChangeCallback(_In_ PVOID context, PMIB_IPINTERFACE_ROW, MIB_NOTIFICATION_TYPE) noexcept;
91 -
92 - std::recursive_mutex m_dnsLock;
93 -
94 - // Flag used when shutting down the object.
95 - _Guarded_by_(m_dnsLock) bool m_stopped = false;
96 -
97 - // Hvsocket channel used to exchange DNS messages with Linux.
98 - DnsTunnelingChannel m_dnsChannel;
99 -
100 - // Queue used to send DNS responses to Linux.
101 - WslCoreMessageQueue m_dnsResponseQueue;
102 -
103 - // Unique id that is incremented for each request. In case the value reaches MAX_UINT and is reset to 0,
104 - // it's assumed previous requests with id's 0, 1, ... finished in the meantime and the id can be reused.
105 - _Guarded_by_(m_dnsLock) uint32_t m_currentRequestId = 0;
106 -
107 - // Mapping request id to the request context structure.
108 - _Guarded_by_(m_dnsLock) std::unordered_map<uint32_t, std::unique_ptr<DnsQueryContext>> m_dnsRequests {};
109 -
110 - // Event that is set when all tracked DNS requests have completed.
111 - wil::unique_event m_allRequestsFinished{wil::EventOptions::ManualReset};
112 -
113 - // Used for handling of external interface constraint setting.
114 - unique_notify_handle m_interfaceNotificationHandle{};
115 -
116 - std::wstring m_externalInterfaceConstraintName;
117 - _Guarded_by_(m_dnsLock) ULONG m_externalInterfaceConstraintIndex = 0;
118 -
119 - const DnsResolverFlags m_flags{};
120 -
121 - // Statistics used for telemetry.
122 - std::atomic<uint32_t> m_totalUdpQueries{0};
123 - std::atomic<uint32_t> m_successfulUdpQueries{0};
124 - std::atomic<uint32_t> m_totalTcpQueries{0};
125 - std::atomic<uint32_t> m_successfulTcpQueries{0};
126 - std::atomic<uint32_t> m_queriesWithNullResult{0};
127 - std::atomic<uint32_t> m_failedDnsQueryRawCalls{0};
128 -
129 - _Guarded_by_(m_dnsLock) std::map<uint32_t, uint32_t> m_dnsApiFailures;
130 -
131 - // Dynamic functions used for calling the DNS APIs.
132 -
133 - // Function to start a raw DNS request.
134 - static std::optional<LxssDynamicFunction<decltype(DnsQueryRaw)>> s_dnsQueryRaw;
135 - // Function to cancel a raw DNS request.
136 - static std::optional<LxssDynamicFunction<decltype(DnsCancelQueryRaw)>> s_dnsCancelQueryRaw;
137 - // Function to free the structure containing the result of a raw DNS request.
138 - static std::optional<LxssDynamicFunction<decltype(DnsQueryRawResultFree)>> s_dnsQueryRawResultFree;
139 -};
140 -
141 -} // namespace wsl::core::networking
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#pragma once
4 +
5 +#include "DnsTunnelingChannel.h"
6 +#include "WslCoreMessageQueue.h"
7 +#include "WslCoreNetworkingSupport.h"
8 +
9 +namespace wsl::core::networking {
10 +
11 +enum class DnsResolverFlags
12 +{
13 + None = 0x0,
14 + BestEffortDnsParsing = 0x1
15 +};
16 +DEFINE_ENUM_FLAG_OPERATORS(DnsResolverFlags);
17 +
18 +class DnsResolver
19 +{
20 +public:
21 + DnsResolver(wil::unique_socket&& dnsHvsocket, DnsResolverFlags flags);
22 + ~DnsResolver() noexcept;
23 +
24 + DnsResolver(const DnsResolver&) = delete;
25 + DnsResolver& operator=(const DnsResolver&) = delete;
26 +
27 + DnsResolver(DnsResolver&&) = delete;
28 + DnsResolver& operator=(DnsResolver&&) = delete;
29 +
30 + void Stop() noexcept;
31 +
32 + static HRESULT LoadDnsResolverMethods() noexcept;
33 +
34 +private:
35 + struct DnsQueryContext
36 + {
37 + // Struct containing protocol (TCP/UDP) and unique id of the Linux DNS client making the request.
38 + LX_GNS_DNS_CLIENT_IDENTIFIER m_dnsClientIdentifier{};
39 +
40 + // Handle used to cancel the request.
41 + DNS_QUERY_RAW_CANCEL m_cancelHandle{};
42 +
43 + // Unique query id.
44 + uint32_t m_id{};
45 +
46 + // Callback to the parent object to notify about the DNS query completion.
47 + std::function<void(DnsQueryContext*, DNS_QUERY_RAW_RESULT*)> m_handleQueryCompletion;
48 +
49 + DnsQueryContext(
50 + uint32_t id,
51 + const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier,
52 + std::function<void(DnsQueryContext*, DNS_QUERY_RAW_RESULT*)>&& handleQueryCompletion) :
53 + m_dnsClientIdentifier(dnsClientIdentifier), m_id(id), m_handleQueryCompletion(std::move(handleQueryCompletion))
54 + {
55 + }
56 +
57 + ~DnsQueryContext() noexcept = default;
58 +
59 + DnsQueryContext(const DnsQueryContext&) = delete;
60 + DnsQueryContext& operator=(const DnsQueryContext&) = delete;
61 + DnsQueryContext(DnsQueryContext&&) = delete;
62 + DnsQueryContext& operator=(DnsQueryContext&&) = delete;
63 + };
64 +
65 + void GenerateTelemetry() noexcept;
66 +
67 + // Process DNS request received from Linux.
68 + //
69 + // Arguments:
70 + // dnsBuffer - buffer containing DNS request.
71 + // dnsClientIdentifier - struct containing protocol (TCP/UDP) and unique id of the Linux DNS client making the request.
72 + void ProcessDnsRequest(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept;
73 +
74 + // Handle completion of DNS query.
75 + //
76 + // Arguments:
77 + // dnsQueryContext - context structure for the DNS request.
78 + // queryResults - structure containing result of the DNS request.
79 + void HandleDnsQueryCompletion(_Inout_ DnsQueryContext* dnsQueryContext, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) noexcept;
80 +
81 + void ResolveExternalInterfaceConstraintIndex() noexcept;
82 +
83 + // Callback that will be invoked by the DNS API whenever a request finishes. The callback is invoked on success, error or when request is cancelled.
84 + //
85 + // Arguments:
86 + // queryContext - pointer to context structure, will be a structure of type DnsQueryContext.
87 + // queryResults - pointer to structure containing the result of the DNS request.
88 + static VOID CALLBACK DnsQueryRawCallback(_In_ VOID* queryContext, _Inout_opt_ DNS_QUERY_RAW_RESULT* queryResults) noexcept;
89 +
90 + static VOID CALLBACK InterfaceChangeCallback(_In_ PVOID context, PMIB_IPINTERFACE_ROW, MIB_NOTIFICATION_TYPE) noexcept;
91 +
92 + std::recursive_mutex m_dnsLock;
93 +
94 + // Flag used when shutting down the object.
95 + _Guarded_by_(m_dnsLock) bool m_stopped = false;
96 +
97 + // Hvsocket channel used to exchange DNS messages with Linux.
98 + DnsTunnelingChannel m_dnsChannel;
99 +
100 + // Queue used to send DNS responses to Linux.
101 + WslCoreMessageQueue m_dnsResponseQueue;
102 +
103 + // Unique id that is incremented for each request. In case the value reaches MAX_UINT and is reset to 0,
104 + // it's assumed previous requests with id's 0, 1, ... finished in the meantime and the id can be reused.
105 + _Guarded_by_(m_dnsLock) uint32_t m_currentRequestId = 0;
106 +
107 + // Mapping request id to the request context structure.
108 + _Guarded_by_(m_dnsLock) std::unordered_map<uint32_t, std::unique_ptr<DnsQueryContext>> m_dnsRequests {};
109 +
110 + // Event that is set when all tracked DNS requests have completed.
111 + wil::unique_event m_allRequestsFinished{wil::EventOptions::ManualReset};
112 +
113 + // Used for handling of external interface constraint setting.
114 + unique_notify_handle m_interfaceNotificationHandle{};
115 +
116 + std::wstring m_externalInterfaceConstraintName;
117 + _Guarded_by_(m_dnsLock) ULONG m_externalInterfaceConstraintIndex = 0;
118 +
119 + const DnsResolverFlags m_flags{};
120 +
121 + // Statistics used for telemetry.
122 + std::atomic<uint32_t> m_totalUdpQueries{0};
123 + std::atomic<uint32_t> m_successfulUdpQueries{0};
124 + std::atomic<uint32_t> m_totalTcpQueries{0};
125 + std::atomic<uint32_t> m_successfulTcpQueries{0};
126 + std::atomic<uint32_t> m_queriesWithNullResult{0};
127 + std::atomic<uint32_t> m_failedDnsQueryRawCalls{0};
128 +
129 + _Guarded_by_(m_dnsLock) std::map<uint32_t, uint32_t> m_dnsApiFailures;
130 +
131 + // Dynamic functions used for calling the DNS APIs.
132 +
133 + // Function to start a raw DNS request.
134 + static std::optional<LxssDynamicFunction<decltype(DnsQueryRaw)>> s_dnsQueryRaw;
135 + // Function to cancel a raw DNS request.
136 + static std::optional<LxssDynamicFunction<decltype(DnsCancelQueryRaw)>> s_dnsCancelQueryRaw;
137 + // Function to free the structure containing the result of a raw DNS request.
138 + static std::optional<LxssDynamicFunction<decltype(DnsQueryRawResultFree)>> s_dnsQueryRawResultFree;
139 +};
140 +
141 +} // namespace wsl::core::networking
src/windows/common/DnsTunnelingChannel.cpp
+115 -115
@@ -1,115 +1,115 @@
1 -// Copyright (C) Microsoft Corporation. All rights reserved.
2 -
3 -#include "precomp.h"
4 -#include "DnsTunnelingChannel.h"
5 -
6 -using wsl::core::networking::DnsTunnelingChannel;
7 -
8 -DnsTunnelingChannel::DnsTunnelingChannel(wil::unique_socket&& socket, DnsTunnelingCallback&& reportDnsRequest) :
9 - m_channel{std::move(socket), "DnsTunneling", m_stopEvent.get()}, m_reportDnsRequest(std::move(reportDnsRequest))
10 -{
11 - WSL_LOG("DnsTunnelingChannel::DnsTunnelingChannel [Windows]", TraceLoggingValue(m_channel.Socket(), "socket"));
12 -
13 - // Start thread waiting for incoming messages from Linux side
14 - m_receiveWorkerThread = std::thread([this]() { ReceiveLoop(); });
15 -}
16 -
17 -DnsTunnelingChannel::~DnsTunnelingChannel()
18 -{
19 - Stop();
20 -}
21 -
22 -void DnsTunnelingChannel::SendDnsMessage(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept
23 -try
24 -{
25 - // Exit if channel was stopped
26 - if (m_stopEvent.is_signaled())
27 - {
28 - return;
29 - }
30 -
31 - wsl::shared::MessageWriter<LX_GNS_DNS_TUNNELING_MESSAGE> message(LxGnsMessageDnsTunneling);
32 - message->DnsClientIdentifier = dnsClientIdentifier;
33 - message.WriteSpan(dnsBuffer);
34 -
35 - m_channel.SendMessage<LX_GNS_DNS_TUNNELING_MESSAGE>(message.Span());
36 -}
37 -CATCH_LOG()
38 -
39 -void DnsTunnelingChannel::ReceiveLoop() noexcept
40 -{
41 - std::vector<gsl::byte> receiveBuffer;
42 -
43 - for (;;)
44 - {
45 - try
46 - {
47 - if (m_stopEvent.is_signaled())
48 - {
49 - return;
50 - }
51 -
52 - WSL_LOG_DEBUG("DnsTunnelingChannel::ReceiveLoop [Windows] - waiting for next message from Linux");
53 -
54 - // Read next message. wsl::shared::socket::RecvMessage() first reads the message header, then uses it to determine the
55 - // total size of the message and read the rest of the message, resizing the buffer if needed.
56 - auto [message, span] = m_channel.ReceiveMessageOrClosed<MESSAGE_HEADER>();
57 - if (message == nullptr)
58 - {
59 - WSL_LOG("DnsTunnelingChannel::ReceiveLoop [Windows] - failed to read message");
60 - return;
61 - }
62 -
63 - // Get the message type from the message header
64 - switch (message->MessageType)
65 - {
66 - case LxGnsMessageDnsTunneling:
67 - {
68 - // Cast message to a LX_GNS_DNS_TUNNELING_MESSAGE struct
69 - auto* dnsMessage = gslhelpers::try_get_struct<LX_GNS_DNS_TUNNELING_MESSAGE>(span);
70 - if (!dnsMessage)
71 - {
72 - WSL_LOG(
73 - "DnsTunnelingChannel::ReceiveLoop [Windows] - failed to convert message to LX_GNS_DNS_TUNNELING_MESSAGE");
74 - return;
75 - }
76 -
77 - // Extract DNS buffer from message
78 - auto dnsBuffer = span.subspan(offsetof(LX_GNS_DNS_TUNNELING_MESSAGE, Buffer));
79 -
80 - WSL_LOG_DEBUG(
81 - "DnsTunnelingChannel::ReceiveLoop [Windows] - received DNS message",
82 - TraceLoggingValue(dnsBuffer.size(), "DNS buffer size"),
83 - TraceLoggingValue(dnsMessage->DnsClientIdentifier.Protocol == IPPROTO_UDP ? "UDP" : "TCP", "Protocol"),
84 - TraceLoggingValue(dnsMessage->DnsClientIdentifier.DnsClientId, "DNS client id"));
85 -
86 - // Invoke callback to notify about the new DNS request
87 - m_reportDnsRequest(dnsBuffer, dnsMessage->DnsClientIdentifier);
88 -
89 - break;
90 - }
91 -
92 - default:
93 - {
94 - THROW_HR_MSG(E_UNEXPECTED, "Unexpected LX_MESSAGE_TYPE : %i", message->MessageType);
95 - }
96 - }
97 - }
98 - CATCH_LOG()
99 - }
100 -}
101 -
102 -void DnsTunnelingChannel::Stop() noexcept
103 -try
104 -{
105 - WSL_LOG("DnsTunnelingChannel::Stop [Windows]");
106 -
107 - m_stopEvent.SetEvent();
108 -
109 - // Stop receive loop
110 - if (m_receiveWorkerThread.joinable())
111 - {
112 - m_receiveWorkerThread.join();
113 - }
114 -}
115 -CATCH_LOG()
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#include "precomp.h"
4 +#include "DnsTunnelingChannel.h"
5 +
6 +using wsl::core::networking::DnsTunnelingChannel;
7 +
8 +DnsTunnelingChannel::DnsTunnelingChannel(wil::unique_socket&& socket, DnsTunnelingCallback&& reportDnsRequest) :
9 + m_channel{std::move(socket), "DnsTunneling", m_stopEvent.get()}, m_reportDnsRequest(std::move(reportDnsRequest))
10 +{
11 + WSL_LOG("DnsTunnelingChannel::DnsTunnelingChannel [Windows]", TraceLoggingValue(m_channel.Socket(), "socket"));
12 +
13 + // Start thread waiting for incoming messages from Linux side
14 + m_receiveWorkerThread = std::thread([this]() { ReceiveLoop(); });
15 +}
16 +
17 +DnsTunnelingChannel::~DnsTunnelingChannel()
18 +{
19 + Stop();
20 +}
21 +
22 +void DnsTunnelingChannel::SendDnsMessage(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept
23 +try
24 +{
25 + // Exit if channel was stopped
26 + if (m_stopEvent.is_signaled())
27 + {
28 + return;
29 + }
30 +
31 + wsl::shared::MessageWriter<LX_GNS_DNS_TUNNELING_MESSAGE> message(LxGnsMessageDnsTunneling);
32 + message->DnsClientIdentifier = dnsClientIdentifier;
33 + message.WriteSpan(dnsBuffer);
34 +
35 + m_channel.SendMessage<LX_GNS_DNS_TUNNELING_MESSAGE>(message.Span());
36 +}
37 +CATCH_LOG()
38 +
39 +void DnsTunnelingChannel::ReceiveLoop() noexcept
40 +{
41 + std::vector<gsl::byte> receiveBuffer;
42 +
43 + for (;;)
44 + {
45 + try
46 + {
47 + if (m_stopEvent.is_signaled())
48 + {
49 + return;
50 + }
51 +
52 + WSL_LOG_DEBUG("DnsTunnelingChannel::ReceiveLoop [Windows] - waiting for next message from Linux");
53 +
54 + // Read next message. wsl::shared::socket::RecvMessage() first reads the message header, then uses it to determine the
55 + // total size of the message and read the rest of the message, resizing the buffer if needed.
56 + auto [message, span] = m_channel.ReceiveMessageOrClosed<MESSAGE_HEADER>();
57 + if (message == nullptr)
58 + {
59 + WSL_LOG("DnsTunnelingChannel::ReceiveLoop [Windows] - failed to read message");
60 + return;
61 + }
62 +
63 + // Get the message type from the message header
64 + switch (message->MessageType)
65 + {
66 + case LxGnsMessageDnsTunneling:
67 + {
68 + // Cast message to a LX_GNS_DNS_TUNNELING_MESSAGE struct
69 + auto* dnsMessage = gslhelpers::try_get_struct<LX_GNS_DNS_TUNNELING_MESSAGE>(span);
70 + if (!dnsMessage)
71 + {
72 + WSL_LOG(
73 + "DnsTunnelingChannel::ReceiveLoop [Windows] - failed to convert message to LX_GNS_DNS_TUNNELING_MESSAGE");
74 + return;
75 + }
76 +
77 + // Extract DNS buffer from message
78 + auto dnsBuffer = span.subspan(offsetof(LX_GNS_DNS_TUNNELING_MESSAGE, Buffer));
79 +
80 + WSL_LOG_DEBUG(
81 + "DnsTunnelingChannel::ReceiveLoop [Windows] - received DNS message",
82 + TraceLoggingValue(dnsBuffer.size(), "DNS buffer size"),
83 + TraceLoggingValue(dnsMessage->DnsClientIdentifier.Protocol == IPPROTO_UDP ? "UDP" : "TCP", "Protocol"),
84 + TraceLoggingValue(dnsMessage->DnsClientIdentifier.DnsClientId, "DNS client id"));
85 +
86 + // Invoke callback to notify about the new DNS request
87 + m_reportDnsRequest(dnsBuffer, dnsMessage->DnsClientIdentifier);
88 +
89 + break;
90 + }
91 +
92 + default:
93 + {
94 + THROW_HR_MSG(E_UNEXPECTED, "Unexpected LX_MESSAGE_TYPE : %i", message->MessageType);
95 + }
96 + }
97 + }
98 + CATCH_LOG()
99 + }
100 +}
101 +
102 +void DnsTunnelingChannel::Stop() noexcept
103 +try
104 +{
105 + WSL_LOG("DnsTunnelingChannel::Stop [Windows]");
106 +
107 + m_stopEvent.SetEvent();
108 +
109 + // Stop receive loop
110 + if (m_receiveWorkerThread.joinable())
111 + {
112 + m_receiveWorkerThread.join();
113 + }
114 +}
115 +CATCH_LOG()
src/windows/common/DnsTunnelingChannel.h
+50 -50
@@ -1,50 +1,50 @@
1 -// Copyright (C) Microsoft Corporation. All rights reserved.
2 -
3 -#pragma once
4 -
5 -#include <wil/resource.h>
6 -#include "lxinitshared.h"
7 -#include "SocketChannel.h"
8 -
9 -namespace wsl::core::networking {
10 -
11 -using DnsTunnelingCallback = std::function<void(const gsl::span<gsl::byte>, const LX_GNS_DNS_CLIENT_IDENTIFIER&)>;
12 -
13 -class DnsTunnelingChannel
14 -{
15 -public:
16 - DnsTunnelingChannel(wil::unique_socket&& socket, DnsTunnelingCallback&& reportDnsRequest);
17 - ~DnsTunnelingChannel();
18 -
19 - DnsTunnelingChannel(const DnsTunnelingChannel&) = delete;
20 - DnsTunnelingChannel& operator=(const DnsTunnelingChannel&) = delete;
21 -
22 - DnsTunnelingChannel(DnsTunnelingChannel&&) = delete;
23 - DnsTunnelingChannel& operator=(DnsTunnelingChannel&&) = delete;
24 -
25 - // Construct and send a LX_GNS_DNS_TUNNELING_MESSAGE message on the channel.
26 - // Note: Callers are responsible for sequencing calls to this method.
27 - //
28 - // Arguments:
29 - // dnsBuffer - buffer containing DNS response.
30 - // dnsClientIdentifier - struct containing protocol (TCP/UDP) and unique id of the Linux DNS client making the request.
31 - void SendDnsMessage(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept;
32 -
33 - // Stop the channel.
34 - void Stop() noexcept;
35 -
36 -private:
37 - // Wait for messages on the channel from Linux side.
38 - void ReceiveLoop() noexcept;
39 -
40 - wil::unique_event m_stopEvent{wil::EventOptions::ManualReset};
41 -
42 - wsl::shared::SocketChannel m_channel;
43 -
44 - std::thread m_receiveWorkerThread;
45 -
46 - // Callback used to notify when there is a new DNS request message on the channel.
47 - DnsTunnelingCallback m_reportDnsRequest;
48 -};
49 -
50 -} // namespace wsl::core::networking
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#pragma once
4 +
5 +#include <wil/resource.h>
6 +#include "lxinitshared.h"
7 +#include "SocketChannel.h"
8 +
9 +namespace wsl::core::networking {
10 +
11 +using DnsTunnelingCallback = std::function<void(const gsl::span<gsl::byte>, const LX_GNS_DNS_CLIENT_IDENTIFIER&)>;
12 +
13 +class DnsTunnelingChannel
14 +{
15 +public:
16 + DnsTunnelingChannel(wil::unique_socket&& socket, DnsTunnelingCallback&& reportDnsRequest);
17 + ~DnsTunnelingChannel();
18 +
19 + DnsTunnelingChannel(const DnsTunnelingChannel&) = delete;
20 + DnsTunnelingChannel& operator=(const DnsTunnelingChannel&) = delete;
21 +
22 + DnsTunnelingChannel(DnsTunnelingChannel&&) = delete;
23 + DnsTunnelingChannel& operator=(DnsTunnelingChannel&&) = delete;
24 +
25 + // Construct and send a LX_GNS_DNS_TUNNELING_MESSAGE message on the channel.
26 + // Note: Callers are responsible for sequencing calls to this method.
27 + //
28 + // Arguments:
29 + // dnsBuffer - buffer containing DNS response.
30 + // dnsClientIdentifier - struct containing protocol (TCP/UDP) and unique id of the Linux DNS client making the request.
31 + void SendDnsMessage(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept;
32 +
33 + // Stop the channel.
34 + void Stop() noexcept;
35 +
36 +private:
37 + // Wait for messages on the channel from Linux side.
38 + void ReceiveLoop() noexcept;
39 +
40 + wil::unique_event m_stopEvent{wil::EventOptions::ManualReset};
41 +
42 + wsl::shared::SocketChannel m_channel;
43 +
44 + std::thread m_receiveWorkerThread;
45 +
46 + // Callback used to notify when there is a new DNS request message on the channel.
47 + DnsTunnelingCallback m_reportDnsRequest;
48 +};
49 +
50 +} // namespace wsl::core::networking
src/windows/common/EnumVariantMap.h new
+339
@@ -0,0 +1,339 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + EnumVariantMap.h
8 +
9 +Abstract:
10 +
11 + Template for enum-based variant maps.
12 +
13 +--*/
14 +#pragma once
15 +#include <map>
16 +#include <type_traits>
17 +#include <utility>
18 +#include <variant>
19 +#include <vector>
20 +
21 +// This template set is used for Arg storage and Context Data storage by enum type.
22 +// The backing storage is a std::multimap of the enum to a variant of types.
23 +// This enables strongly typed storage and retrieval of values based on an enum key.
24 +namespace wsl::windows::wslc {
25 +
26 +// Enum based variant helper.
27 +// Enum must be an enum whose first member has the value 0, each subsequent member increases by 1, and the final member is named Max.
28 +// Mapping is a template type that takes one template parameter of type Enum, and whose members define value_t as the type for that enum value.
29 +template <typename Enum, template <Enum> typename Mapping>
30 +struct EnumBasedVariant
31 +{
32 +private:
33 + // Used to deduce the variant type; making a variant that includes std::monostate and all Mapping types.
34 + template <size_t... I>
35 + static inline auto Deduce(std::index_sequence<I...>)
36 + {
37 + return std::variant<std::monostate, typename Mapping<static_cast<Enum>(I)>::value_t...>{};
38 + }
39 +
40 +public:
41 + // Holds data of any type listed in Mapping.
42 + using variant_t = decltype(Deduce(std::make_index_sequence<static_cast<size_t>(Enum::Max)>()));
43 +
44 + // Gets the index into the variant for the given Data.
45 + static constexpr inline size_t Index(Enum e)
46 + {
47 + return static_cast<size_t>(e) + 1;
48 + }
49 +};
50 +
51 +// An action that can be taken on an EnumBasedVariantMap.
52 +enum class EnumBasedVariantMapAction
53 +{
54 + Add,
55 + Contains,
56 + Get,
57 + GetAll,
58 + Count,
59 + Remove,
60 +};
61 +
62 +// A callback function that can be used for logging map actions.
63 +template <typename Enum>
64 +using EnumBasedVariantMapActionCallback = void (*)(const void* map, Enum value, EnumBasedVariantMapAction action);
65 +
66 +// Forward declaration for EnumBasedVariantMapEmplacer
67 +template <typename Enum, template <Enum> typename Mapping, typename V>
68 +struct EnumBasedVariantMapEmplacer;
69 +
70 +// Provides a multimap of the Enum to the mapped types (allows multiple values per key).
71 +template <typename Enum, template <Enum> typename Mapping, EnumBasedVariantMapActionCallback<Enum> Callback = nullptr>
72 +struct EnumBasedVariantMap
73 +{
74 + using Variant = EnumBasedVariant<Enum, Mapping>;
75 +
76 + template <Enum E>
77 + using mapping_t = typename Mapping<E>::value_t;
78 +
79 + // Adds a value to the map. With multimap, this always adds a new entry (doesn't overwrite).
80 + template <Enum E>
81 + void Add(mapping_t<E>&& v)
82 + {
83 + if constexpr (Callback)
84 + {
85 + Callback(this, E, EnumBasedVariantMapAction::Add);
86 + }
87 +
88 + // Compile-time type checking - this should always pass since mapping_t<E> is the correct type
89 + using CleanV = std::remove_cvref_t<mapping_t<E>>;
90 + static_assert(
91 + std::is_same_v<CleanV, mapping_t<E>>,
92 + "Type mismatch in Add: provided type does not match the expected type for this enum value");
93 +
94 + typename Variant::variant_t variant;
95 + variant.template emplace<Variant::Index(E)>(std::move(v));
96 + m_data.emplace(E, std::move(variant));
97 + }
98 +
99 + template <Enum E>
100 + void Add(const mapping_t<E>& v)
101 + {
102 + if constexpr (Callback)
103 + {
104 + Callback(this, E, EnumBasedVariantMapAction::Add);
105 + }
106 +
107 + // Compile-time type checking - this should always pass since mapping_t<E> is the correct type
108 + using CleanV = std::remove_cvref_t<mapping_t<E>>;
109 + static_assert(
110 + std::is_same_v<CleanV, mapping_t<E>>,
111 + "Type mismatch in Add: provided type does not match the expected type for this enum value");
112 +
113 + typename Variant::variant_t variant;
114 + variant.template emplace<Variant::Index(E)>(v);
115 + m_data.emplace(E, std::move(variant));
116 + }
117 +
118 + // Runtime version of Add that takes the enum as a parameter.
119 + template <typename V>
120 + void Add(Enum e, V&& v)
121 + {
122 + if constexpr (Callback)
123 + {
124 + Callback(this, e, EnumBasedVariantMapAction::Add);
125 + }
126 +
127 + // Check if the type matches the SPECIFIC enum value at compile time if possible
128 + using CleanV = std::remove_cvref_t<V>;
129 +
130 + // Pre-check if this type matches the specific enum value being added to
131 + if (!IsMatchingType<CleanV>(e))
132 + {
133 + THROW_HR_MSG(E_INVALIDARG, "Type mismatch: provided type does not match the expected type for enum value %d", static_cast<int>(e));
134 + }
135 +
136 + typename Variant::variant_t variant;
137 + EmplaceAtRuntimeIndex(variant, e, std::forward<V>(v), std::make_index_sequence<static_cast<size_t>(Enum::Max)>());
138 + m_data.emplace(e, std::move(variant));
139 + }
140 +
141 + // Runtime method to check if value V matches the mapped type for an enum value.
142 + template <typename V>
143 + bool IsMatchingType(Enum e) const
144 + {
145 + return IsMatchingTypeImpl<V>(e, std::make_index_sequence<static_cast<size_t>(Enum::Max)>());
146 + }
147 +
148 + // Return a value indicating whether the given enum has at least one entry.
149 + bool Contains(Enum e) const
150 + {
151 + if constexpr (Callback)
152 + {
153 + Callback(this, e, EnumBasedVariantMapAction::Contains);
154 + }
155 + return (m_data.find(e) != m_data.end());
156 + }
157 +
158 + // Gets the count of values for a specific enum key.
159 + size_t Count(Enum e) const
160 + {
161 + if constexpr (Callback)
162 + {
163 + Callback(this, e, EnumBasedVariantMapAction::Count);
164 + }
165 + return m_data.count(e);
166 + }
167 +
168 + // Gets the FIRST value for the enum key (for backward compatibility).
169 + // Non-const version returns a reference that can be modified.
170 + template <Enum E>
171 + mapping_t<E>& Get()
172 + {
173 + if constexpr (Callback)
174 + {
175 + Callback(this, E, EnumBasedVariantMapAction::Get);
176 + }
177 + auto itr = m_data.find(E);
178 + THROW_HR_IF_MSG(E_NOT_SET, itr == m_data.end(), "Get(%d): key not found", static_cast<int>(E));
179 +
180 + // Validate that the variant holds the expected type at the expected index
181 + constexpr size_t expectedIndex = Variant::Index(E);
182 + if (itr->second.index() != expectedIndex)
183 + {
184 + THROW_HR_MSG(
185 + E_UNEXPECTED,
186 + "Get(%d): variant type mismatch - expected index %zu, got %zu",
187 + static_cast<int>(E),
188 + expectedIndex,
189 + itr->second.index());
190 + }
191 +
192 + return std::get<expectedIndex>(itr->second);
193 + }
194 +
195 + // Const overload of Get, cannot be modified.
196 + template <Enum E>
197 + const mapping_t<E>& Get() const
198 + {
199 + if constexpr (Callback)
200 + {
201 + Callback(this, E, EnumBasedVariantMapAction::Get);
202 + }
203 + auto itr = m_data.find(E);
204 + THROW_HR_IF_MSG(E_NOT_SET, itr == m_data.cend(), "Get(%d): key not found", static_cast<int>(E));
205 +
206 + // Validate that the variant holds the expected type at the expected index
207 + constexpr size_t expectedIndex = Variant::Index(E);
208 + if (itr->second.index() != expectedIndex)
209 + {
210 + THROW_HR_MSG(
211 + E_UNEXPECTED,
212 + "Get(%d): variant type mismatch - expected index %zu, got %zu",
213 + static_cast<int>(E),
214 + expectedIndex,
215 + itr->second.index());
216 + }
217 +
218 + return std::get<expectedIndex>(itr->second);
219 + }
220 +
221 + // Gets ALL values for a specific enum key as a vector.
222 + template <Enum E>
223 + std::vector<mapping_t<E>> GetAll() const
224 + {
225 + if constexpr (Callback)
226 + {
227 + Callback(this, E, EnumBasedVariantMapAction::GetAll);
228 + }
229 +
230 + std::vector<mapping_t<E>> results;
231 + auto range = m_data.equal_range(E);
232 +
233 + for (auto it = range.first; it != range.second; ++it)
234 + {
235 + results.push_back(std::get<Variant::Index(E)>(it->second));
236 + }
237 +
238 + return results;
239 + }
240 +
241 + // Removes ALL entries for a specific enum key.
242 + void Remove(Enum e)
243 + {
244 + if constexpr (Callback)
245 + {
246 + Callback(this, e, EnumBasedVariantMapAction::Remove);
247 + }
248 + m_data.erase(e);
249 + }
250 +
251 + // Gets the total number of items stored (across all keys).
252 + size_t GetCount() const
253 + {
254 + return m_data.size();
255 + }
256 +
257 + // Gets a vector of all UNIQUE enum keys stored in the map.
258 + std::vector<Enum> GetKeys() const
259 + {
260 + std::vector<Enum> keys;
261 + Enum lastKey = static_cast<Enum>(-1);
262 + bool first = true;
263 +
264 + for (const auto& pair : m_data)
265 + {
266 + if (first || pair.first != lastKey)
267 + {
268 + keys.push_back(pair.first);
269 + lastKey = pair.first;
270 + first = false;
271 + }
272 + }
273 +
274 + return keys;
275 + }
276 +
277 +private:
278 + // Helper to implement runtime type checking.
279 + template <typename V, size_t... I>
280 + bool IsMatchingTypeImpl(Enum e, std::index_sequence<I...>) const
281 + {
282 + bool result = false;
283 + ((static_cast<size_t>(e) == I ? (result = std::is_same_v<std::remove_cvref_t<V>, mapping_t<static_cast<Enum>(I)>>, true) : false) || ...);
284 + return result;
285 + }
286 +
287 + // Helper to emplace at runtime-determined index
288 + template <typename V, size_t... I>
289 + void EmplaceAtRuntimeIndex(typename Variant::variant_t& variant, Enum e, V&& v, std::index_sequence<I...>)
290 + {
291 + size_t index = static_cast<size_t>(e) + 1;
292 + bool handled = false;
293 +
294 + (
295 + [&] {
296 + if (index == I + 1 && !handled)
297 + {
298 + using Emplacer = wsl::windows::wslc::EnumBasedVariantMapEmplacer<Enum, Mapping, V>;
299 + Emplacer::template Emplace<I + 1>(variant, std::forward<V>(v));
300 + handled = true;
301 + }
302 + }(),
303 + ...);
304 +
305 + if (!handled)
306 + {
307 + using CleanV = std::remove_cvref_t<V>;
308 + THROW_HR_MSG(E_INVALIDARG, "Invalid enum value: %d", static_cast<int>(e));
309 + }
310 + }
311 +
312 + std::multimap<Enum, typename Variant::variant_t> m_data;
313 +};
314 +
315 +// Helper for runtime emplacement into std::variant for EnumBasedVariantMap
316 +template <typename Enum, template <Enum> typename Mapping, typename V>
317 +struct EnumBasedVariantMapEmplacer
318 +{
319 + template <size_t Index>
320 + static void Emplace(typename EnumBasedVariant<Enum, Mapping>::variant_t& variant, V&& value)
321 + {
322 + using TargetType = typename Mapping<static_cast<Enum>(Index - 1)>::value_t;
323 + using CleanV = std::remove_cvref_t<V>;
324 +
325 + constexpr bool is_same_type = std::is_same_v<CleanV, TargetType>;
326 + constexpr bool is_convertible = std::is_convertible_v<CleanV, TargetType>;
327 + constexpr bool is_constructible = std::is_constructible_v<TargetType, CleanV>;
328 +
329 + if constexpr (is_same_type || is_convertible || is_constructible)
330 + {
331 + variant.template emplace<Index>(std::forward<V>(value));
332 + }
333 + else
334 + {
335 + throw std::runtime_error("Runtime type mismatch: cannot convert value to target type for this enum value");
336 + }
337 + }
338 +};
339 +} // namespace wsl::windows::wslc
src/windows/common/ExecutionContext.cpp
+73 -8
@@ -5,6 +5,7 @@
5 #include "wsleventschema.h"
6
7 using wsl::windows::common::ClientExecutionContext;
8 +using wsl::windows::common::COMServiceExecutionContext;
9 using wsl::windows::common::Context;
10 using wsl::windows::common::Error;
11 using wsl::windows::common::ExecutionContext;
@@ -13,13 +14,15 @@ using wsl::windows::common::ServiceExecutionContext;
14 thread_local ExecutionContext* g_currentContext = nullptr;
15 static bool g_enabled = false;
16 bool g_runningInService = false;
17 +bool g_useComErrors = false;
18 static HANDLE g_eventLog = nullptr;
19
18 -void wsl::windows::common::EnableContextualizedErrors(bool service)
20 +void wsl::windows::common::EnableContextualizedErrors(bool service, bool useComErrors)
21 {
22 WI_ASSERT(!g_enabled);
23 g_enabled = true;
24 g_runningInService = service;
25 + g_useComErrors = useComErrors;
26 }
27
28 ExecutionContext::ExecutionContext(Context context, FILE* warningsFile) noexcept :
@@ -39,6 +42,7 @@ ExecutionContext::~ExecutionContext()
42 {
43 g_currentContext = m_parent;
44 WI_ASSERT(!m_errorString.has_value());
45 + WI_ASSERT(!m_errorSource.has_value());
46 }
47
48 ExecutionContext* ExecutionContext::Current()
@@ -46,10 +50,11 @@ ExecutionContext* ExecutionContext::Current()
50 return g_currentContext;
51 }
52
49 -void ExecutionContext::SetErrorStringImpl(std::wstring&& string)
53 +void ExecutionContext::SetErrorStringImpl(std::wstring&& string, std::wstring&& source)
54 {
55 WI_ASSERT(!m_errorString.has_value());
56 m_errorString = std::move(string);
57 + m_errorSource = std::move(source);
58 }
59
60 bool ExecutionContext::CanCollectUserErrorMessage()
@@ -84,7 +89,7 @@ ULONGLONG ExecutionContext::CurrentContext() const noexcept
89 return errorContext;
90 }
91
87 -void ExecutionContext::CollectErrorImpl(HRESULT result, ULONGLONG context, std::optional<std::wstring>&& message)
92 +void ExecutionContext::CollectErrorImpl(HRESULT result, ULONGLONG context, std::optional<std::wstring>&& message, std::optional<std::wstring>&& source)
93 {
94 WI_ASSERT(m_parent == nullptr);
95
@@ -107,12 +112,13 @@ void ExecutionContext::CollectErrorImpl(HRESULT result, ULONGLONG context, std::
112 */
113
114 m_error->Message = std::move(message);
115 + m_error->Source = std::move(source);
116 }
117
118 return;
119 }
120
115 - m_error.emplace(result, context, std::move(message));
121 + m_error.emplace(result, context, std::move(message), std::move(source));
122 }
123
124 void ExecutionContext::CollectError(HRESULT result)
@@ -127,9 +133,27 @@ void ExecutionContext::CollectError(HRESULT result)
133
134 void ExecutionContext::CollectErrorImpl(HRESULT result)
135 {
130 - RootContext().CollectErrorImpl(result, CurrentContext(), std::move(m_errorString));
136 + if (!g_runningInService && g_useComErrors && !m_errorString.has_value() && !m_errorSource.has_value())
137 + {
138 + // If no error message has been reported, look for a COM error.
139 + if (auto comError = common::wslutil::GetCOMErrorInfo())
140 + {
141 + if (comError->Message)
142 + {
143 + m_errorString = comError->Message.get();
144 + }
145 +
146 + if (comError->Source)
147 + {
148 + m_errorSource = comError->Source.get();
149 + }
150 + }
151 + }
152 +
153 + RootContext().CollectErrorImpl(result, CurrentContext(), std::move(m_errorString), std::move(m_errorSource));
154
155 m_errorString.reset();
156 + m_errorSource.reset();
157 }
158
159 void ExecutionContext::EmitUserWarning(const std::wstring& warning, const std::source_location& location)
@@ -246,7 +270,7 @@ void ClientExecutionContext::CollectErrorImpl(HRESULT result)
270 message = std::wstring(m_outError.Message);
271 }
272
249 - RootContext().CollectErrorImpl(result, errorContext, std::move(message));
273 + RootContext().CollectErrorImpl(result, errorContext, std::move(message), {});
274 }
275
276 void ClientExecutionContext::FlushWarnings()
@@ -362,19 +386,60 @@ ServiceExecutionContext::~ServiceExecutionContext()
386 }
387 }
388
389 +COMServiceExecutionContext::COMServiceExecutionContext() : ExecutionContext(Empty)
390 +{
391 +}
392 +
393 +COMServiceExecutionContext::~COMServiceExecutionContext()
394 +try
395 +{
396 + if (m_error.has_value())
397 + {
398 + wil::com_ptr<ICreateErrorInfo> errorInfo;
399 + THROW_IF_FAILED(CreateErrorInfo(&errorInfo));
400 +
401 + if (m_error->Message.has_value())
402 + {
403 + auto description = wil::make_bstr(m_error->Message->c_str());
404 + THROW_IF_FAILED(errorInfo->SetDescription(description.get()));
405 + }
406 +
407 + if (m_error->Source.has_value())
408 + {
409 + THROW_IF_FAILED(errorInfo->SetSource(wil::make_bstr(m_error->Source->c_str()).get()));
410 + }
411 +
412 + if (m_error->Source.has_value() || m_error->Message.has_value())
413 + {
414 + THROW_IF_FAILED(SetErrorInfo(0, errorInfo.query<IErrorInfo>().get()));
415 + }
416 + }
417 +}
418 +CATCH_LOG(); // Catch to avoid throwing from a destructor
419 +
420 +bool COMServiceExecutionContext::CanCollectUserErrorMessage()
421 +{
422 + return true;
423 +}
424 +
425 LXSS_ERROR_INFO* ClientExecutionContext::OutError() noexcept
426 {
427 return &m_outError;
428 }
429
370 -void wsl::windows::common::SetErrorMessage(std::wstring&& message)
430 +void wsl::windows::common::SetErrorMessage(std::string&& message, const std::source_location& source)
431 +{
432 + return SetErrorMessage(wsl::shared::string::MultiByteToWide(message), source);
433 +}
434 +
435 +void wsl::windows::common::SetErrorMessage(std::wstring&& message, const std::source_location& source)
436 {
437 if (g_currentContext == nullptr || message.empty())
438 {
439 return; // no context to save the error to or empty message, ignore
440 }
441
377 - g_currentContext->SetErrorStringImpl(std::move(message));
442 + g_currentContext->SetErrorStringImpl(std::move(message), std::format(L"{}", source));
443 }
444
445 void wsl::windows::common::SetEventLog(HANDLE eventLog)
src/windows/common/ExecutionContext.h
+48 -8
@@ -2,14 +2,37 @@
2
3 #pragma once
4
5 +#include "wslutil.h"
6 +
7 namespace wsl::windows::common {
8
9 #define THROW_HR_WITH_USER_ERROR(Result, Message) \
8 - if (wsl::windows::common::ExecutionContext::ShouldCollectErrorMessage()) \
10 + do \
11 + { \
12 + auto _messageWide = std::format(L"{}", Message); \
13 + if (wsl::windows::common::ExecutionContext::ShouldCollectErrorMessage()) \
14 + { \
15 + ::wsl::windows::common::SetErrorMessage(std::wstring(_messageWide)); \
16 + } \
17 + THROW_HR_MSG(Result, "%ls", _messageWide.c_str()); \
18 + } while (false);
19 +
20 +#define THROW_HR_WITH_USER_ERROR_MSG(Result, Message, Format, ...) \
21 + do \
22 + { \
23 + auto _messageWide = std::format(L"{}", Message); \
24 + if (wsl::windows::common::ExecutionContext::ShouldCollectErrorMessage()) \
25 + { \
26 + ::wsl::windows::common::SetErrorMessage(std::wstring(_messageWide)); \
27 + } \
28 + THROW_HR_MSG(Result, "%ls. " Format, _messageWide.c_str(), ##__VA_ARGS__); \
29 + } while (false);
30 +
31 +#define THROW_HR_WITH_USER_ERROR_IF(Result, Message, Condition) \
32 + if (Condition) \
33 { \
10 - ::wsl::windows::common::SetErrorMessage(Message); \
11 - } \
12 - THROW_HR(Result)
34 + THROW_HR_WITH_USER_ERROR(Result, Message); \
35 + }
36
37 #define EMIT_USER_WARNING(Warning) \
38 if (::wsl::windows::common::ExecutionContext* context = ::wsl::windows::common::ExecutionContext::Current(); context != nullptr) \
@@ -66,6 +89,7 @@ enum Context : ULONGLONG
89 UpdatePackage = 0x10000000000,
90 QueryLatestGitHubRelease = 0x20000000000,
91 VerifyChecksum = 0x40000000000,
92 + WslC = 0x80000000000,
93 };
94
95 DEFINE_ENUM_FLAG_OPERATORS(Context)
@@ -75,6 +99,7 @@ struct Error
99 HRESULT Code = E_UNEXPECTED;
100 ULONGLONG Context = 0;
101 std::optional<std::wstring> Message;
102 + std::optional<std::wstring> Source;
103 };
104
105 /*
@@ -105,11 +130,11 @@ public:
130 bool CanCollectUserWarnings() const;
131 void EmitUserWarning(const std::wstring& warning, const std::source_location& location = std::source_location::current());
132
108 - void CollectErrorImpl(HRESULT result, ULONGLONG context, std::optional<std::wstring>&& message);
133 + void CollectErrorImpl(HRESULT result, ULONGLONG context, std::optional<std::wstring>&& message, std::optional<std::wstring>&& source);
134
135 const std::optional<Error>& ReportedError() const noexcept;
136
112 - void SetErrorStringImpl(std::wstring&& string);
137 + void SetErrorStringImpl(std::wstring&& string, std::wstring&& source);
138
139 ULONGLONG CurrentContext() const noexcept;
140
@@ -127,6 +152,7 @@ private:
152 ExecutionContext* m_parent = nullptr;
153 Context m_context = Context::Empty;
154 std::optional<std::wstring> m_errorString;
155 + std::optional<std::wstring> m_errorSource;
156 };
157
158 class ClientExecutionContext : public ExecutionContext
@@ -178,9 +204,23 @@ private:
204 wil::unique_handle m_warningsPipe;
205 };
206
181 -void EnableContextualizedErrors(bool service);
207 +class COMServiceExecutionContext : public ExecutionContext
208 +{
209 +
210 +public:
211 + NON_COPYABLE(COMServiceExecutionContext);
212 + NON_MOVABLE(COMServiceExecutionContext);
213 +
214 + COMServiceExecutionContext();
215 + ~COMServiceExecutionContext() override;
216 +
217 + bool CanCollectUserErrorMessage() override;
218 +};
219 +
220 +void EnableContextualizedErrors(bool service, bool useComErrors = false);
221
183 -void SetErrorMessage(std::wstring&& message);
222 +void SetErrorMessage(std::wstring&& message, const std::source_location& source = std::source_location::current());
223 +void SetErrorMessage(std::string&& message, const std::source_location& source = std::source_location::current());
224
225 void SetEventLog(HANDLE eventLog);
226
src/windows/common/GuestDeviceManager.cpp
+17 -6
@@ -9,6 +9,15 @@ GuestDeviceManager::GuestDeviceManager(_In_ const std::wstring& machineId, _In_
9 {
10 }
11
12 +GuestDeviceManager::~GuestDeviceManager()
13 +{
14 + try
15 + {
16 + m_deviceHostSupport->Shutdown();
17 + }
18 + CATCH_LOG()
19 +}
20 +
21 _Requires_lock_not_held_(m_lock)
22 GUID GuestDeviceManager::AddGuestDevice(
23 _In_ const GUID& DeviceId, _In_ const GUID& ImplementationClsid, _In_ PCWSTR AccessName, _In_opt_ PCWSTR Options, _In_ PCWSTR Path, _In_ UINT32 Flags, _In_ HANDLE UserToken)
@@ -40,18 +49,20 @@ GUID GuestDeviceManager::AddHdvShareWithOptions(
49 if (!server)
50 {
51 server = wil::CoCreateInstance<IPlan9FileSystem>(ImplementationClsid, (CLSCTX_LOCAL_SERVER | CLSCTX_ENABLE_CLOAKING | CLSCTX_ENABLE_AAA));
43 - AddRemoteFileSystem(ImplementationClsid, c_defaultDeviceTag.c_str(), server);
52 + m_deviceHostSupport->AddRemoteFileSystem(ImplementationClsid, c_defaultDeviceTag.c_str(), server);
53 }
54
55 THROW_IF_FAILED(server->AddSharePath(nameWithOptions.c_str(), Path, Flags));
56 }
57
58 // This requires more privileges than the user may have, so impersonation is disabled.
50 - return AddNewDevice(DeviceId, server, AccessName);
59 + return m_deviceHostSupport->AddNewDevice(DeviceId, server, AccessName);
60 }
61
62 +_Requires_lock_not_held_(m_lock)
63 GUID GuestDeviceManager::AddNewDevice(_In_ const GUID& deviceId, _In_ const wil::com_ptr<IPlan9FileSystem>& server, _In_ PCWSTR tag)
64 {
65 + auto guestDeviceLock = m_lock.lock_exclusive();
66 return m_deviceHostSupport->AddNewDevice(deviceId, server, tag);
67 }
68
@@ -135,9 +146,9 @@ wil::com_ptr<IPlan9FileSystem> GuestDeviceManager::GetRemoteFileSystem(_In_ REFC
146 return m_deviceHostSupport->GetRemoteFileSystem(clsid, tag);
147 }
148
138 -void GuestDeviceManager::Shutdown()
139 -try
149 +_Requires_lock_not_held_(m_lock)
150 +void GuestDeviceManager::RemoveGuestDevice(_In_ const GUID& DeviceId, _In_ const GUID& InstanceId)
151 {
141 - m_deviceHostSupport->Shutdown();
152 + auto guestDeviceLock = m_lock.lock_exclusive();
153 + m_deviceHostSupport->RemoveDevice(DeviceId, InstanceId);
154 }
143 -CATCH_LOG()
src/windows/common/GuestDeviceManager.h
+4 -1
@@ -28,6 +28,7 @@ class GuestDeviceManager
28 {
29 public:
30 GuestDeviceManager(_In_ const std::wstring& machineId, _In_ const GUID& runtimeId);
31 + ~GuestDeviceManager();
32
33 _Requires_lock_not_held_(m_lock)
34 GUID AddGuestDevice(
@@ -39,6 +40,7 @@ public:
40 _In_ UINT32 Flags,
41 _In_ HANDLE UserToken);
42
43 + _Requires_lock_not_held_(m_lock)
44 GUID AddNewDevice(_In_ const GUID& deviceId, _In_ const wil::com_ptr<IPlan9FileSystem>& server, _In_ PCWSTR tag);
45
46 void AddRemoteFileSystem(_In_ REFCLSID clsid, _In_ PCWSTR tag, _In_ const wil::com_ptr<IPlan9FileSystem>& server);
@@ -47,7 +49,8 @@ public:
49
50 wil::com_ptr<IPlan9FileSystem> GetRemoteFileSystem(_In_ REFCLSID clsid, _In_ std::wstring_view tag);
51
50 - void Shutdown();
52 + _Requires_lock_not_held_(m_lock)
53 + void RemoveGuestDevice(_In_ const GUID& DeviceId, _In_ const GUID& InstanceId);
54
55 private:
56 _Requires_lock_held_(m_lock)
src/windows/common/RingBuffer.cpp
+153 -153
@@ -1,154 +1,154 @@
1 -/*++
2 -
3 -Copyright (c) Microsoft. All rights reserved.
4 -
5 -Module Name:
6 -
7 - RingBuffer.cpp
8 -
9 -Abstract:
10 -
11 - This file contains definitions for the RingBuffer class.
12 -
13 ---*/
14 -
15 -#include "precomp.h"
16 -#include "RingBuffer.h"
17 -
18 -RingBuffer::RingBuffer(size_t size) : m_maxSize(size), m_offset(0)
19 -{
20 - m_buffer.reserve(size);
21 -}
22 -
23 -void RingBuffer::Insert(std::string_view data)
24 -{
25 - auto lock = m_lock.lock_exclusive();
26 - auto remainingData = gsl::make_span(data.data(), data.size());
27 - if (remainingData.size() > m_maxSize)
28 - {
29 - remainingData = remainingData.subspan(remainingData.size() - m_maxSize);
30 - }
31 -
32 - const auto bytesAtEnd = std::min(m_maxSize - m_offset, remainingData.size());
33 - if (m_offset + bytesAtEnd > m_buffer.size())
34 - {
35 - m_buffer.resize(m_offset + bytesAtEnd);
36 - WI_ASSERT(m_buffer.size() <= m_maxSize);
37 - }
38 -
39 - const auto allBuffer = gsl::make_span(m_buffer);
40 - const auto beginCopyBuffer = allBuffer.subspan(m_offset, bytesAtEnd);
41 - copy(remainingData.subspan(0, bytesAtEnd), beginCopyBuffer);
42 - remainingData = remainingData.subspan(bytesAtEnd);
43 - if (!remainingData.empty())
44 - {
45 - copy(remainingData, allBuffer);
46 - m_offset = remainingData.size();
47 - }
48 - else
49 - {
50 - m_offset += bytesAtEnd;
51 - }
52 -}
53 -
54 -std::vector<std::string> RingBuffer::GetLastDelimitedStrings(char Delimiter, size_t Count) const
55 -{
56 - auto lock = m_lock.lock_shared();
57 - auto [begin, end] = Contents();
58 - std::vector<std::string> results;
59 - std::optional<size_t> endIndex;
60 - for (size_t i = end.size(); i > 0; i--)
61 - {
62 - if (results.size() == Count)
63 - {
64 - break;
65 - }
66 -
67 - if (Delimiter == end[i - 1])
68 - {
69 - if (endIndex.has_value())
70 - {
71 - results.emplace(results.begin(), &end[i], endIndex.value() - i);
72 - endIndex.reset();
73 - }
74 - else
75 - {
76 - endIndex = i - 1;
77 - }
78 - }
79 - }
80 -
81 - if (results.size() == Count)
82 - {
83 - return results;
84 - }
85 -
86 - std::string partial;
87 - if (endIndex.has_value())
88 - {
89 - partial = std::string{&end[0], endIndex.value()};
90 - endIndex.reset();
91 - }
92 -
93 - for (size_t i = begin.size(); i > 0; i--)
94 - {
95 - if (results.size() == Count)
96 - {
97 - break;
98 - }
99 -
100 - if (Delimiter == begin[i - 1])
101 - {
102 - if (!partial.empty())
103 - {
104 - // The debug CRT will fastfail if begin[size] is accessed
105 - // But in this case it's not a problem because begin.size() - i would be == 0
106 - std::string partial_begin{&begin.data()[i], begin.size() - i};
107 - results.emplace(results.begin(), partial_begin + partial);
108 - partial.clear();
109 - }
110 - else if (endIndex.has_value())
111 - {
112 - results.emplace(results.begin(), &begin.data()[i], endIndex.value() - i);
113 - endIndex.reset();
114 - }
115 - else
116 - {
117 - endIndex = i - 1;
118 - }
119 - }
120 - }
121 -
122 - if (results.size() < Count)
123 - {
124 - // May have lost some data, or this could be the very first line logged.
125 - if (!partial.empty())
126 - {
127 - results.emplace(results.begin(), partial);
128 - }
129 - else if (endIndex.has_value())
130 - {
131 - results.emplace(results.begin(), &begin[0], endIndex.value());
132 - }
133 - }
134 -
135 - return results;
136 -}
137 -
138 -std::string RingBuffer::Get() const
139 -{
140 - auto lock = m_lock.lock_shared();
141 - auto [begin, end] = Contents();
142 - std::string data;
143 - data.reserve(begin.size() + end.size());
144 - data.append(begin.data(), begin.size());
145 - data.append(end.data(), end.size());
146 - return data;
147 -}
148 -
149 -std::pair<std::string_view, std::string_view> RingBuffer::Contents() const
150 -{
151 - std::string_view beginView(m_buffer.data() + m_offset, m_buffer.size() - m_offset);
152 - std::string_view endView(m_buffer.data(), m_offset);
153 - return {beginView, endView};
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RingBuffer.cpp
8 +
9 +Abstract:
10 +
11 + This file contains definitions for the RingBuffer class.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "RingBuffer.h"
17 +
18 +RingBuffer::RingBuffer(size_t size) : m_maxSize(size), m_offset(0)
19 +{
20 + m_buffer.reserve(size);
21 +}
22 +
23 +void RingBuffer::Insert(std::string_view data)
24 +{
25 + auto lock = m_lock.lock_exclusive();
26 + auto remainingData = gsl::make_span(data.data(), data.size());
27 + if (remainingData.size() > m_maxSize)
28 + {
29 + remainingData = remainingData.subspan(remainingData.size() - m_maxSize);
30 + }
31 +
32 + const auto bytesAtEnd = std::min(m_maxSize - m_offset, remainingData.size());
33 + if (m_offset + bytesAtEnd > m_buffer.size())
34 + {
35 + m_buffer.resize(m_offset + bytesAtEnd);
36 + WI_ASSERT(m_buffer.size() <= m_maxSize);
37 + }
38 +
39 + const auto allBuffer = gsl::make_span(m_buffer);
40 + const auto beginCopyBuffer = allBuffer.subspan(m_offset, bytesAtEnd);
41 + copy(remainingData.subspan(0, bytesAtEnd), beginCopyBuffer);
42 + remainingData = remainingData.subspan(bytesAtEnd);
43 + if (!remainingData.empty())
44 + {
45 + copy(remainingData, allBuffer);
46 + m_offset = remainingData.size();
47 + }
48 + else
49 + {
50 + m_offset += bytesAtEnd;
51 + }
52 +}
53 +
54 +std::vector<std::string> RingBuffer::GetLastDelimitedStrings(char Delimiter, size_t Count) const
55 +{
56 + auto lock = m_lock.lock_shared();
57 + auto [begin, end] = Contents();
58 + std::vector<std::string> results;
59 + std::optional<size_t> endIndex;
60 + for (size_t i = end.size(); i > 0; i--)
61 + {
62 + if (results.size() == Count)
63 + {
64 + break;
65 + }
66 +
67 + if (Delimiter == end[i - 1])
68 + {
69 + if (endIndex.has_value())
70 + {
71 + results.emplace(results.begin(), &end[i], endIndex.value() - i);
72 + endIndex.reset();
73 + }
74 + else
75 + {
76 + endIndex = i - 1;
77 + }
78 + }
79 + }
80 +
81 + if (results.size() == Count)
82 + {
83 + return results;
84 + }
85 +
86 + std::string partial;
87 + if (endIndex.has_value())
88 + {
89 + partial = std::string{&end[0], endIndex.value()};
90 + endIndex.reset();
91 + }
92 +
93 + for (size_t i = begin.size(); i > 0; i--)
94 + {
95 + if (results.size() == Count)
96 + {
97 + break;
98 + }
99 +
100 + if (Delimiter == begin[i - 1])
101 + {
102 + if (!partial.empty())
103 + {
104 + // The debug CRT will fastfail if begin[size] is accessed
105 + // But in this case it's not a problem because begin.size() - i would be == 0
106 + std::string partial_begin{&begin.data()[i], begin.size() - i};
107 + results.emplace(results.begin(), partial_begin + partial);
108 + partial.clear();
109 + }
110 + else if (endIndex.has_value())
111 + {
112 + results.emplace(results.begin(), &begin.data()[i], endIndex.value() - i);
113 + endIndex.reset();
114 + }
115 + else
116 + {
117 + endIndex = i - 1;
118 + }
119 + }
120 + }
121 +
122 + if (results.size() < Count)
123 + {
124 + // May have lost some data, or this could be the very first line logged.
125 + if (!partial.empty())
126 + {
127 + results.emplace(results.begin(), partial);
128 + }
129 + else if (endIndex.has_value())
130 + {
131 + results.emplace(results.begin(), &begin[0], endIndex.value());
132 + }
133 + }
134 +
135 + return results;
136 +}
137 +
138 +std::string RingBuffer::Get() const
139 +{
140 + auto lock = m_lock.lock_shared();
141 + auto [begin, end] = Contents();
142 + std::string data;
143 + data.reserve(begin.size() + end.size());
144 + data.append(begin.data(), begin.size());
145 + data.append(end.data(), end.size());
146 + return data;
147 +}
148 +
149 +std::pair<std::string_view, std::string_view> RingBuffer::Contents() const
150 +{
151 + std::string_view beginView(m_buffer.data() + m_offset, m_buffer.size() - m_offset);
152 + std::string_view endView(m_buffer.data(), m_offset);
153 + return {beginView, endView};
154 }
\ No newline at end of file
src/windows/common/RingBuffer.h
+33 -33
@@ -1,34 +1,34 @@
1 -/*++
2 -
3 -Copyright (c) Microsoft. All rights reserved.
4 -
5 -Module Name:
6 -
7 - RingBuffer.h
8 -
9 -Abstract:
10 -
11 - This file contains declarations for the RingBuffer class.
12 -
13 ---*/
14 -
15 -#pragma once
16 -
17 -class RingBuffer
18 -{
19 -public:
20 - RingBuffer() = delete;
21 - RingBuffer(size_t size);
22 -
23 - void Insert(std::string_view data);
24 - std::vector<std::string> GetLastDelimitedStrings(char Delimiter, size_t Count) const;
25 - std::string Get() const;
26 -
27 -private:
28 - std::pair<std::string_view, std::string_view> Contents() const;
29 -
30 - mutable wil::srwlock m_lock;
31 - std::vector<char> m_buffer;
32 - size_t m_maxSize;
33 - size_t m_offset;
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RingBuffer.h
8 +
9 +Abstract:
10 +
11 + This file contains declarations for the RingBuffer class.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +class RingBuffer
18 +{
19 +public:
20 + RingBuffer() = delete;
21 + RingBuffer(size_t size);
22 +
23 + void Insert(std::string_view data);
24 + std::vector<std::string> GetLastDelimitedStrings(char Delimiter, size_t Count) const;
25 + std::string Get() const;
26 +
27 +private:
28 + std::pair<std::string_view, std::string_view> Contents() const;
29 +
30 + mutable wil::srwlock m_lock;
31 + std::vector<char> m_buffer;
32 + size_t m_maxSize;
33 + size_t m_offset;
34 };
\ No newline at end of file
src/windows/common/SubProcess.cpp
+38 -44
@@ -16,36 +16,9 @@ Abstract:
16
17 #include "SubProcess.h"
18
19 +using namespace wsl::windows::common::relay;
20 using wsl::windows::common::SubProcess;
21
21 -namespace {
22 -wil::unique_file FileFromHandle(wil::unique_hfile& Handle, const char* Mode)
23 -{
24 - using UniqueFd = wil::unique_any<int, decltype(_close), _close, wil::details::pointer_access_all, int, int, -1>;
25 -
26 - UniqueFd Fd(_open_osfhandle(reinterpret_cast<intptr_t>(Handle.get()), 0));
27 - THROW_LAST_ERROR_IF(Fd.get() < 0);
28 -
29 - Handle.release();
30 -
31 - wil::unique_file File(_fdopen(Fd.get(), Mode));
32 - THROW_LAST_ERROR_IF(!File);
33 - Fd.release();
34 -
35 - return File;
36 -}
37 -
38 -std::wstring ReadFileContent(wil::unique_hfile& Handle)
39 -{
40 - THROW_LAST_ERROR_IF(SetFilePointer(Handle.get(), 0, 0, FILE_BEGIN) == INVALID_SET_FILE_POINTER);
41 -
42 - const auto File = FileFromHandle(Handle, "r");
43 -
44 - std::ifstream Stdout(File.get());
45 - return wsl::shared::string::MultiByteToWide(std::string(std::istreambuf_iterator<char>(Stdout), {}));
46 -}
47 -} // namespace
48 -
22 SubProcess::SubProcess(LPCWSTR ApplicationName, LPCWSTR CommandLine, DWORD Flags, DWORD StartupFlags) :
23 m_applicationName(ApplicationName), m_commandLine(CommandLine), m_flags(Flags), m_startupFlags(StartupFlags)
24 {
@@ -240,29 +213,50 @@ DWORD SubProcess::Run(DWORD Timeout)
213
214 SubProcess::ProcessOutput SubProcess::RunAndCaptureOutput(DWORD Timeout, HANDLE StdErr)
215 {
243 - //
244 - // Using pipes could cause a deadlock if the process writes more bytes
245 - // than the size of the pipe buffer. Using two files to prevent that.
246 - //
216 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
217 + // Clear out references to stdout and stderr pipes.
218 + m_stdOut = nullptr;
219 + m_stdErr = nullptr;
220 + });
221 +
222 + auto [stdoutRead, stdoutWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, true, false);
223 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(stdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
224 +
225 + m_stdOut = stdoutWrite.get();
226 +
227 + relay::MultiHandleWait io;
228 + std::string stdoutNative;
229 + std::string stderrNative;
230
248 - using wsl::windows::common::filesystem::TempFile;
249 - const auto flags = filesystem::TempFileFlags::DeleteOnClose | filesystem::TempFileFlags::InheritHandle;
250 - auto stdoutFile = filesystem::TempFile(GENERIC_ALL, 0, OPEN_EXISTING, flags);
251 - m_stdOut = stdoutFile.Handle.get();
231 + io.AddHandle(std::make_unique<relay::ReadHandle>(
232 + std::move(stdoutRead), [&](const gsl::span<char>& buffer) { stdoutNative.append(buffer.data(), buffer.size()); }));
233
253 - std::optional<filesystem::TempFile> stderrFile;
234 + wil::unique_hfile stderrWrite;
235 if (StdErr == nullptr)
236 {
256 - stderrFile = filesystem::TempFile(GENERIC_ALL, 0, OPEN_EXISTING, flags);
237 + wil::unique_hfile stderrRead;
238 + std::tie(stderrRead, stderrWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(0, true, false);
239 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(stderrWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
240 +
241 + m_stdErr = stderrWrite.get();
242 +
243 + io.AddHandle(std::make_unique<relay::ReadHandle>(
244 + std::move(stderrRead), [&](const gsl::span<char>& buffer) { stderrNative.append(buffer.data(), buffer.size()); }));
245 }
246 + else
247 + {
248 + m_stdErr = StdErr;
249 + }
250 +
251 + auto process = Start();
252 + stdoutWrite.reset();
253 + stderrWrite.reset();
254
259 - m_stdErr = stderrFile ? stderrFile->Handle.get() : StdErr;
255 + io.Run(std::chrono::milliseconds{Timeout});
256
261 - const DWORD ExitCode = GetExitCode(Start().get(), Timeout);
262 - ProcessOutput output{ExitCode, ReadFileContent(stdoutFile.Handle), stderrFile ? ReadFileContent(stderrFile->Handle) : L""};
257 + // Reusing the same timeout since the std handles have been fully read at that point.
258 + const DWORD ExitCode = GetExitCode(process.get(), Timeout);
259 + ProcessOutput output{ExitCode, shared::string::MultiByteToWide(stdoutNative), shared::string::MultiByteToWide(stderrNative)};
260
264 - // Clear out references to stdout and stderr temp files.
265 - m_stdOut = nullptr;
266 - m_stdErr = nullptr;
261 return output;
262 }
\ No newline at end of file
src/windows/common/VirtioNetworking.cpp
+26 -5
@@ -15,13 +15,29 @@ static constexpr auto c_eth0DeviceName = L"eth0";
15 static constexpr auto c_loopbackDeviceName = TEXT(LX_INIT_LOOPBACK_DEVICE_NAME);
16
17 VirtioNetworking::VirtioNetworking(
18 - GnsChannel&& gnsChannel, VirtioNetworkingFlags flags, LPCWSTR dnsOptions, std::shared_ptr<GuestDeviceManager> guestDeviceManager, wil::shared_handle userToken) :
18 + GnsChannel&& gnsChannel,
19 + VirtioNetworkingFlags flags,
20 + LPCWSTR dnsOptions,
21 + std::shared_ptr<GuestDeviceManager> guestDeviceManager,
22 + wil::shared_handle userToken,
23 + wil::unique_socket&& dnsHvsocket) :
24 m_guestDeviceManager(std::move(guestDeviceManager)),
25 m_userToken(std::move(userToken)),
26 m_gnsChannel(std::move(gnsChannel)),
27 m_flags(flags),
28 m_dnsOptions(dnsOptions)
29 {
30 + THROW_HR_IF_MSG(
31 + E_INVALIDARG,
32 + ((!!dnsHvsocket != WI_IsFlagSet(m_flags, VirtioNetworkingFlags::DnsTunnelingSocket)) ||
33 + (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::DnsTunnelingSocket) && WI_IsFlagSet(m_flags, VirtioNetworkingFlags::DnsTunneling))),
34 + "Incompatible DNS settings");
35 +
36 + if (dnsHvsocket)
37 + {
38 + networking::DnsResolverFlags resolverFlags{};
39 + m_dnsTunnelingResolver.emplace(std::move(dnsHvsocket), resolverFlags);
40 + }
41 }
42
43 VirtioNetworking::~VirtioNetworking()
@@ -72,9 +88,11 @@ void VirtioNetworking::StartPortTracker(wil::unique_socket&& socket)
88 }
89
90 void NETIOAPI_API_ VirtioNetworking::OnNetworkConnectivityChange(PVOID context, NL_NETWORK_CONNECTIVITY_HINT hint)
91 +try
92 {
93 static_cast<VirtioNetworking*>(context)->RefreshGuestConnection();
94 }
95 +CATCH_LOG()
96
97 HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int protocol, bool allocate) const noexcept
98 {
@@ -164,8 +182,7 @@ int VirtioNetworking::ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET&
182 return 0;
183 }
184
167 -void VirtioNetworking::RefreshGuestConnection() noexcept
168 -try
185 +void VirtioNetworking::RefreshGuestConnection()
186 {
187 // Query current networking information before acquiring the lock.
188 auto networkSettings = GetHostEndpointSettings();
@@ -196,6 +213,10 @@ try
213 {
214 currentDns = networking::HostDnsInfo::GetDnsTunnelingSettings(default_route);
215 }
216 + else if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::DnsTunnelingSocket))
217 + {
218 + currentDns = networking::HostDnsInfo::GetDnsTunnelingSettings(TEXT(LX_INIT_DNS_TUNNELING_IP_ADDRESS));
219 + }
220 else
221 {
222 wsl::core::networking::DnsSettingsFlags dnsFlags = networking::DnsSettingsFlags::IncludeVpn;
@@ -211,7 +232,6 @@ try
232 // Add virtio net adapter to guest. If the adapter already exists update adapter state.
233 if (device_options != m_trackedDeviceOptions)
234 {
214 - m_trackedDeviceOptions = device_options;
235 if (!m_adapterId.has_value())
236 {
237 m_adapterId = m_guestDeviceManager->AddGuestDevice(
@@ -225,6 +245,8 @@ try
245 LOG_IF_FAILED(server->AddSharePath(c_eth0DeviceName, device_options.c_str(), 0));
246 }
247 }
248 +
249 + m_trackedDeviceOptions = device_options;
250 }
251
252 UpdateIpv4Address(networkSettings->PreferredIpAddress);
@@ -240,7 +262,6 @@ try
262
263 m_networkSettings = std::move(networkSettings);
264 }
243 -CATCH_LOG();
265
266 void VirtioNetworking::SetupLoopbackDevice()
267 {
src/windows/common/VirtioNetworking.h
+12 -2
@@ -4,6 +4,7 @@
4
5 #include "INetworkingEngine.h"
6 #include "GnsChannel.h"
7 +#include "DnsResolver.h"
8 #include "WslCoreHostDnsInfo.h"
9 #include "GnsPortTrackerChannel.h"
10 #include "GuestDeviceManager.h"
@@ -16,13 +17,21 @@ enum class VirtioNetworkingFlags
17 LocalhostRelay = 0x1,
18 DnsTunneling = 0x2,
19 Ipv6 = 0x4,
20 + DnsTunnelingSocket = 0x8,
21 };
22 DEFINE_ENUM_FLAG_OPERATORS(VirtioNetworkingFlags);
23
24 class VirtioNetworking : public INetworkingEngine
25 {
26 public:
25 - VirtioNetworking(GnsChannel&& gnsChannel, VirtioNetworkingFlags flags, LPCWSTR dnsOptions, std::shared_ptr<GuestDeviceManager> guestDeviceManager, wil::shared_handle userToken);
27 + VirtioNetworking(
28 + GnsChannel&& gnsChannel,
29 + VirtioNetworkingFlags flags,
30 + LPCWSTR dnsOptions,
31 + std::shared_ptr<GuestDeviceManager> guestDeviceManager,
32 + wil::shared_handle userToken,
33 + wil::unique_socket&& dnsHvsocket = {});
34 +
35 ~VirtioNetworking();
36
37 // Note: This class cannot be moved because m_networkNotifyHandle captures a 'this' pointer.
@@ -42,7 +51,7 @@ private:
51
52 HRESULT HandlePortNotification(const SOCKADDR_INET& addr, int protocol, bool allocate) const noexcept;
53 int ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& addr, _In_ int protocol, _In_ bool isOpen) const;
45 - void RefreshGuestConnection() noexcept;
54 + void RefreshGuestConnection();
55 void SetupLoopbackDevice();
56 void SendDefaultRoute(const std::wstring& gateway, wsl::shared::hns::ModifyRequestType requestType);
57 void SendIpv6Address(const networking::EndpointIpAddress& ipAddress, wsl::shared::hns::ModifyRequestType requestType);
@@ -61,6 +70,7 @@ private:
70 std::shared_ptr<networking::NetworkSettings> m_networkSettings;
71 VirtioNetworkingFlags m_flags = VirtioNetworkingFlags::None;
72 LPCWSTR m_dnsOptions = nullptr;
73 + std::optional<networking::DnsResolver> m_dnsTunnelingResolver;
74 std::optional<GUID> m_localhostAdapterId;
75 std::optional<GUID> m_adapterId;
76
src/windows/common/WSLCContainerLauncher.cpp new
+372
@@ -0,0 +1,372 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCContainerLauncher.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the implementation for WSLCContainerLauncher.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "WSLCContainerLauncher.h"
17 +
18 +using wsl::windows::common::ClientRunningWSLCProcess;
19 +using wsl::windows::common::RunningWSLCContainer;
20 +using wsl::windows::common::WSLCContainerLauncher;
21 +
22 +RunningWSLCContainer::RunningWSLCContainer(wil::com_ptr<IWSLCContainer>&& Container, WSLCProcessFlags Flags) :
23 + m_container(std::move(Container)), m_flags(Flags)
24 +{
25 +}
26 +
27 +RunningWSLCContainer::~RunningWSLCContainer()
28 +{
29 + Reset();
30 +}
31 +
32 +IWSLCContainer& RunningWSLCContainer::Get()
33 +{
34 + return *m_container;
35 +}
36 +
37 +void RunningWSLCContainer::Reset()
38 +{
39 + if (m_container && m_deleteOnClose)
40 + {
41 + // Attempt to stop and delete the container.
42 + LOG_IF_FAILED(m_container->Delete(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes));
43 + }
44 +
45 + m_container.reset();
46 +}
47 +
48 +WSLCContainerState RunningWSLCContainer::State()
49 +{
50 + WSLCContainerState state{};
51 + THROW_IF_FAILED(m_container->GetState(&state));
52 + return state;
53 +}
54 +
55 +ClientRunningWSLCProcess RunningWSLCContainer::GetInitProcess()
56 +{
57 + wil::com_ptr<IWSLCProcess> process;
58 + THROW_IF_FAILED(m_container->GetInitProcess(&process));
59 +
60 + return ClientRunningWSLCProcess{std::move(process), m_flags};
61 +}
62 +
63 +void RunningWSLCContainer::SetDeleteOnClose(bool deleteOnClose)
64 +{
65 + m_deleteOnClose = deleteOnClose;
66 +}
67 +
68 +std::string RunningWSLCContainer::Id()
69 +{
70 + WSLCContainerId id{};
71 + THROW_IF_FAILED(m_container->GetId(id));
72 +
73 + return id;
74 +}
75 +
76 +std::string RunningWSLCContainer::Name()
77 +{
78 + wil::unique_cotaskmem_ansistring name;
79 + THROW_IF_FAILED(m_container->GetName(&name));
80 +
81 + return name.get();
82 +}
83 +
84 +WSLCContainerLauncher::WSLCContainerLauncher(
85 + const std::string& Image,
86 + const std::string& Name,
87 + const std::vector<std::string>& Arguments,
88 + const std::vector<std::string>& Environment,
89 + WSLCContainerNetworkType containerNetworkType,
90 + WSLCProcessFlags Flags) :
91 + WSLCProcessLauncher({}, Arguments, Environment, Flags), m_image(Image), m_name(Name), m_containerNetworkType(containerNetworkType)
92 +{
93 +}
94 +
95 +void WSLCContainerLauncher::AddPort(uint16_t WindowsPort, uint16_t ContainerPort, int Family, int Protocol, const std::optional<std::string>& BindingAddress)
96 +{
97 + THROW_HR_IF(E_INVALIDARG, Family != AF_INET && Family != AF_INET6);
98 +
99 + WSLCPortMapping port{
100 + .HostPort = WindowsPort,
101 + .ContainerPort = ContainerPort,
102 + .Family = Family,
103 + .Protocol = Protocol,
104 + };
105 +
106 + if (BindingAddress.has_value())
107 + {
108 + THROW_HR_IF(E_INVALIDARG, BindingAddress->size() > WSLC_MAX_BINDING_ADDRESS_LENGTH);
109 + THROW_HR_IF_MSG(
110 + E_INVALIDARG, strcpy_s(port.BindingAddress, BindingAddress->c_str()) != 0, "Invalid address: %hs", BindingAddress->c_str());
111 + }
112 + else
113 + {
114 + static_assert(sizeof("127.0.0.1") <= WSLC_MAX_BINDING_ADDRESS_LENGTH + 1, "Default IPv4 binding address too long");
115 + static_assert(sizeof("::1") <= WSLC_MAX_BINDING_ADDRESS_LENGTH + 1, "Default IPv6 binding address too long");
116 + THROW_HR_IF(E_INVALIDARG, strcpy_s(port.BindingAddress, Family == AF_INET ? "127.0.0.1" : "::1") != 0);
117 + }
118 +
119 + m_ports.push_back(port);
120 +}
121 +
122 +void WSLCContainerLauncher::SetName(std::string&& Name)
123 +{
124 + m_name = std::move(Name);
125 +}
126 +
127 +void WSLCContainerLauncher::SetDefaultStopSignal(WSLCSignal Signal)
128 +{
129 + m_stopSignal = Signal;
130 +}
131 +
132 +void WSLCContainerLauncher::SetEntrypoint(std::vector<std::string>&& entrypoint)
133 +{
134 + m_entrypoint = std::move(entrypoint);
135 +}
136 +
137 +void WSLCContainerLauncher::SetContainerFlags(WSLCContainerFlags Flags)
138 +{
139 + m_containerFlags = Flags;
140 +}
141 +
142 +void WSLCContainerLauncher::SetHostname(std::string&& Hostname)
143 +{
144 + m_hostname = std::move(Hostname);
145 +}
146 +
147 +void WSLCContainerLauncher::SetDomainname(std::string&& Domainame)
148 +{
149 + m_domainname = std::move(Domainame);
150 +}
151 +
152 +void WSLCContainerLauncher::SetDnsServers(std::vector<std::string>&& DnsServers)
153 +{
154 + m_dnsServers = std::move(DnsServers);
155 +}
156 +
157 +void WSLCContainerLauncher::SetDnsSearchDomains(std::vector<std::string>&& DnsSearchDomains)
158 +{
159 + m_dnsSearchDomains = std::move(DnsSearchDomains);
160 +}
161 +
162 +void WSLCContainerLauncher::SetDnsOptions(std::vector<std::string>&& DnsOptions)
163 +{
164 + m_dnsOptions = std::move(DnsOptions);
165 +}
166 +
167 +void wsl::windows::common::WSLCContainerLauncher::AddVolume(const std::wstring& HostPath, const std::string& ContainerPath, bool ReadOnly)
168 +{
169 + // Store a copy of the path strings to the launcher to ensure the pointers in WSLCVolume remain valid.
170 + const auto& hostPath = m_hostPaths.emplace_back(HostPath);
171 + const auto& containerPath = m_containerPaths.emplace_back(ContainerPath);
172 +
173 + WSLCVolume vol{};
174 + vol.HostPath = hostPath.c_str();
175 + vol.ContainerPath = containerPath.c_str();
176 + vol.ReadOnly = ReadOnly ? TRUE : FALSE;
177 +
178 + m_volumes.push_back(vol);
179 +}
180 +
181 +void wsl::windows::common::WSLCContainerLauncher::AddNamedVolume(const std::string& Name, const std::string& ContainerPath, bool ReadOnly)
182 +{
183 + const auto& name = m_volumeNames.emplace_back(Name);
184 + const auto& containerPath = m_containerPaths.emplace_back(ContainerPath);
185 +
186 + WSLCNamedVolume volume{};
187 + volume.Name = name.c_str();
188 + volume.ContainerPath = containerPath.c_str();
189 + volume.ReadOnly = ReadOnly ? TRUE : FALSE;
190 +
191 + m_namedVolumes.push_back(volume);
192 +}
193 +
194 +void wsl::windows::common::WSLCContainerLauncher::AddLabel(const std::string& Key, const std::string& Value)
195 +{
196 + // Store a copy of the key/value strings to the launcher to ensure the pointers in WSLCLabel remain valid.
197 + const auto& key = m_labelKeys.emplace_back(Key);
198 + const auto& value = m_labelValues.emplace_back(Value);
199 +
200 + WSLCLabel label{};
201 + label.Key = key.c_str();
202 + label.Value = value.c_str();
203 +
204 + m_labels.push_back(label);
205 +}
206 +
207 +void wsl::windows::common::WSLCContainerLauncher::AddTmpfs(const std::string& ContainerPath, const std::string& Options)
208 +{
209 + // Store a copy of the path/options strings to the launcher to ensure the pointers in WSLCTmpfsMount remain valid.
210 + const auto& containerPath = m_tmpfsContainerPaths.emplace_back(ContainerPath);
211 + const auto& options = m_tmpfsOptions.emplace_back(Options);
212 +
213 + WSLCTmpfsMount tmpfs{};
214 + tmpfs.Destination = containerPath.c_str();
215 + tmpfs.Options = options.c_str();
216 +
217 + m_tmpfsMounts.push_back(tmpfs);
218 +}
219 +
220 +std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::LaunchNoThrow(IWSLCSession& Session, WSLCContainerStartFlags Flags)
221 +{
222 + auto [result, container] = CreateNoThrow(Session);
223 + if (FAILED(result))
224 + {
225 + return std::make_pair(result, std::optional<RunningWSLCContainer>{});
226 + }
227 +
228 + result = container.value().Get().Start(Flags, nullptr);
229 +
230 + return std::make_pair(result, std::move(container));
231 +}
232 +
233 +std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::CreateNoThrow(IWSLCSession& Session)
234 +{
235 + WSLCContainerOptions options{};
236 + options.Image = m_image.c_str();
237 +
238 + if (!m_name.empty())
239 + {
240 + options.Name = m_name.c_str();
241 + }
242 +
243 + std::vector<const char*> entrypointStorage;
244 +
245 + for (const auto& e : m_entrypoint)
246 + {
247 + entrypointStorage.push_back(e.c_str());
248 + }
249 +
250 + auto [processOptions, commandLinePtrs, environmentPtrs] = CreateProcessOptions();
251 + options.InitProcessOptions = processOptions;
252 + options.ContainerNetwork.ContainerNetworkType = m_containerNetworkType;
253 + options.Ports = m_ports.data();
254 + options.PortsCount = static_cast<ULONG>(m_ports.size());
255 + options.StopSignal = m_stopSignal;
256 + options.Flags = m_containerFlags;
257 +
258 + if (!entrypointStorage.empty())
259 + {
260 + options.Entrypoint = {entrypointStorage.data(), static_cast<ULONG>(entrypointStorage.size())};
261 + }
262 +
263 + if (!m_hostname.empty())
264 + {
265 + options.HostName = m_hostname.c_str();
266 + }
267 +
268 + if (!m_domainname.empty())
269 + {
270 + options.DomainName = m_domainname.c_str();
271 + }
272 +
273 + std::vector<const char*> dnsServersStorage;
274 + for (const auto& e : m_dnsServers)
275 + {
276 + dnsServersStorage.push_back(e.c_str());
277 + }
278 +
279 + if (!dnsServersStorage.empty())
280 + {
281 + options.DnsServers = {dnsServersStorage.data(), static_cast<ULONG>(dnsServersStorage.size())};
282 + }
283 +
284 + std::vector<const char*> dnsSearchDomainsStorage;
285 + for (const auto& e : m_dnsSearchDomains)
286 + {
287 + dnsSearchDomainsStorage.push_back(e.c_str());
288 + }
289 +
290 + if (!dnsSearchDomainsStorage.empty())
291 + {
292 + options.DnsSearchDomains = {dnsSearchDomainsStorage.data(), static_cast<ULONG>(dnsSearchDomainsStorage.size())};
293 + }
294 +
295 + std::vector<const char*> dnsOptionsStorage;
296 + for (const auto& e : m_dnsOptions)
297 + {
298 + dnsOptionsStorage.push_back(e.c_str());
299 + }
300 +
301 + if (!dnsOptionsStorage.empty())
302 + {
303 + options.DnsOptions = {dnsOptionsStorage.data(), static_cast<ULONG>(dnsOptionsStorage.size())};
304 + }
305 +
306 + if (!m_workingDirectory.empty())
307 + {
308 + options.InitProcessOptions.CurrentDirectory = m_workingDirectory.c_str();
309 + }
310 +
311 + options.VolumesCount = static_cast<ULONG>(m_volumes.size());
312 + options.Volumes = m_volumes.size() > 0 ? m_volumes.data() : nullptr;
313 +
314 + options.NamedVolumesCount = static_cast<ULONG>(m_namedVolumes.size());
315 + options.NamedVolumes = m_namedVolumes.size() > 0 ? m_namedVolumes.data() : nullptr;
316 +
317 + options.LabelsCount = static_cast<ULONG>(m_labels.size());
318 + options.Labels = m_labels.size() > 0 ? m_labels.data() : nullptr;
319 +
320 + options.TmpfsCount = static_cast<ULONG>(m_tmpfsMounts.size());
321 + options.Tmpfs = m_tmpfsMounts.size() > 0 ? m_tmpfsMounts.data() : nullptr;
322 +
323 + // TODO: Support volumes, ports, flags, shm size, container networking mode, etc.
324 + wil::com_ptr<IWSLCContainer> container;
325 + auto result = Session.CreateContainer(&options, &container);
326 + if (FAILED(result))
327 + {
328 + return std::pair<HRESULT, std::optional<RunningWSLCContainer>>(result, std::optional<RunningWSLCContainer>{});
329 + }
330 +
331 + return std::make_pair(S_OK, std::move(RunningWSLCContainer{std::move(container), m_flags}));
332 +}
333 +
334 +RunningWSLCContainer WSLCContainerLauncher::Create(IWSLCSession& Session)
335 +{
336 + auto [result, container] = CreateNoThrow(Session);
337 + THROW_IF_FAILED(result);
338 +
339 + return std::move(container.value());
340 +}
341 +
342 +RunningWSLCContainer WSLCContainerLauncher::Launch(IWSLCSession& Session, WSLCContainerStartFlags Flags)
343 +{
344 + auto [result, container] = LaunchNoThrow(Session, Flags);
345 + THROW_IF_FAILED(result);
346 +
347 + return std::move(container.value());
348 +}
349 +
350 +wsl::windows::common::wslc_schema::InspectContainer RunningWSLCContainer::Inspect()
351 +{
352 + wil::unique_cotaskmem_ansistring output;
353 + THROW_IF_FAILED(m_container->Inspect(&output));
354 +
355 + return wsl::shared::FromJson<wslc_schema::InspectContainer>(output.get());
356 +}
357 +
358 +std::map<std::string, std::string> RunningWSLCContainer::Labels()
359 +{
360 + wil::unique_cotaskmem_array_ptr<WSLCLabelInformation> labels;
361 + THROW_IF_FAILED(m_container->GetLabels(&labels, labels.size_address<ULONG>()));
362 +
363 + std::map<std::string, std::string> result;
364 + for (size_t i = 0; i < labels.size(); i++)
365 + {
366 + result[labels[i].Key] = labels[i].Value;
367 + CoTaskMemFree(labels[i].Key);
368 + CoTaskMemFree(labels[i].Value);
369 + }
370 +
371 + return result;
372 +}
src/windows/common/WSLCContainerLauncher.h new
+111
@@ -0,0 +1,111 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCContainerLauncher.h
8 +
9 +Abstract:
10 +
11 + This file contains the definition for WSLCContainerLauncher.
12 +
13 +--*/
14 +
15 +#pragma once
16 +#include "WSLCProcessLauncher.h"
17 +#include "docker_schema.h"
18 +#include "wslc_schema.h"
19 +
20 +namespace wsl::windows::common {
21 +
22 +class RunningWSLCContainer
23 +{
24 +public:
25 + NON_COPYABLE(RunningWSLCContainer);
26 + DEFAULT_MOVABLE(RunningWSLCContainer);
27 + RunningWSLCContainer(wil::com_ptr<IWSLCContainer>&& Container, WSLCProcessFlags Flags);
28 + ~RunningWSLCContainer();
29 + IWSLCContainer& Get();
30 +
31 + WSLCContainerState State();
32 + ClientRunningWSLCProcess GetInitProcess();
33 + void SetDeleteOnClose(bool deleteOnClose);
34 + void Reset();
35 + wslc_schema::InspectContainer Inspect();
36 + std::string Id();
37 + std::string Name();
38 + std::map<std::string, std::string> Labels();
39 +
40 +private:
41 + wil::com_ptr<IWSLCContainer> m_container;
42 + WSLCProcessFlags m_flags;
43 + bool m_deleteOnClose = true;
44 +};
45 +
46 +class WSLCContainerLauncher : private WSLCProcessLauncher
47 +{
48 +public:
49 + NON_COPYABLE(WSLCContainerLauncher);
50 + NON_MOVABLE(WSLCContainerLauncher);
51 +
52 + WSLCContainerLauncher(
53 + const std::string& Image,
54 + const std::string& Name = "",
55 + const std::vector<std::string>& Arguments = {},
56 + const std::vector<std::string>& Environment = {},
57 + WSLCContainerNetworkType containerNetworkType = WSLCContainerNetworkTypeHost,
58 + WSLCProcessFlags Flags = WSLCProcessFlagsNone);
59 +
60 + void AddVolume(const std::wstring& HostPath, const std::string& ContainerPath, bool ReadOnly);
61 + void AddNamedVolume(const std::string& Name, const std::string& ContainerPath, bool ReadOnly);
62 + void AddPort(uint16_t WindowsPort, uint16_t ContainerPort, int Family, int Protocol = IPPROTO_TCP, const std::optional<std::string>& BindingAddress = {});
63 + void AddLabel(const std::string& Key, const std::string& Value);
64 + void AddTmpfs(const std::string& ContainerPath, const std::string& Options);
65 +
66 + std::pair<HRESULT, std::optional<RunningWSLCContainer>> CreateNoThrow(IWSLCSession& Session);
67 + RunningWSLCContainer Create(IWSLCSession& Session);
68 +
69 + RunningWSLCContainer Launch(IWSLCSession& Session, WSLCContainerStartFlags Flags = WSLCContainerStartFlagsAttach);
70 + std::pair<HRESULT, std::optional<RunningWSLCContainer>> LaunchNoThrow(IWSLCSession& Session, WSLCContainerStartFlags Flags = WSLCContainerStartFlagsAttach);
71 +
72 + void SetName(std::string&& Name);
73 + void SetEntrypoint(std::vector<std::string>&& entrypoint);
74 + void SetDefaultStopSignal(WSLCSignal Signal);
75 + void SetContainerFlags(WSLCContainerFlags Flags);
76 + void SetHostname(std::string&& Hostname);
77 + void SetDomainname(std::string&& Domainame);
78 + void SetDnsServers(std::vector<std::string>&& DnsServers);
79 + void SetDnsSearchDomains(std::vector<std::string>&& DnsSearchDomains);
80 + void SetDnsOptions(std::vector<std::string>&& DnsOptions);
81 +
82 + using WSLCProcessLauncher::FormatResult;
83 + using WSLCProcessLauncher::SetUser;
84 + using WSLCProcessLauncher::SetWorkingDirectory;
85 +
86 +private:
87 + std::string m_image;
88 + std::string m_name;
89 + std::vector<WSLCPortMapping> m_ports;
90 + std::vector<WSLCVolume> m_volumes;
91 + std::vector<WSLCNamedVolume> m_namedVolumes;
92 + std::deque<std::wstring> m_hostPaths;
93 + std::deque<std::string> m_volumeNames;
94 + std::deque<std::string> m_containerPaths;
95 + WSLCContainerNetworkType m_containerNetworkType;
96 + std::vector<std::string> m_entrypoint;
97 + WSLCSignal m_stopSignal = WSLCSignalNone;
98 + WSLCContainerFlags m_containerFlags = WSLCContainerFlagsNone;
99 + std::string m_hostname;
100 + std::string m_domainname;
101 + std::vector<std::string> m_dnsServers;
102 + std::vector<std::string> m_dnsSearchDomains;
103 + std::vector<std::string> m_dnsOptions;
104 + std::vector<WSLCLabel> m_labels;
105 + std::deque<std::string> m_labelKeys;
106 + std::deque<std::string> m_labelValues;
107 + std::vector<WSLCTmpfsMount> m_tmpfsMounts;
108 + std::deque<std::string> m_tmpfsContainerPaths;
109 + std::deque<std::string> m_tmpfsOptions;
110 +};
111 +} // namespace wsl::windows::common
\ No newline at end of file
src/windows/common/WSLCProcessLauncher.cpp new
+241
@@ -0,0 +1,241 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCProcessLauncher.cpp
8 +
9 +Abstract:
10 +
11 + WSLCProcessLauncher implementation.
12 +
13 +--*/
14 +
15 +#include <precomp.h>
16 +#include "WSLCProcessLauncher.h"
17 +
18 +using wsl::windows::common::ClientRunningWSLCProcess;
19 +using wsl::windows::common::RunningWSLCProcess;
20 +using wsl::windows::common::WSLCProcessLauncher;
21 +
22 +WSLCProcessLauncher::WSLCProcessLauncher(
23 + const std::string& Executable, const std::vector<std::string>& Arguments, const std::vector<std::string>& Environment, WSLCProcessFlags Flags) :
24 + m_executable(Executable), m_arguments(Arguments), m_environment(Environment), m_flags(Flags)
25 +{
26 +}
27 +
28 +void WSLCProcessLauncher::SetTtySize(ULONG Rows, ULONG Columns)
29 +{
30 + m_rows = Rows;
31 + m_columns = Columns;
32 +}
33 +
34 +void WSLCProcessLauncher::SetWorkingDirectory(std::string&& WorkingDirectory)
35 +{
36 + m_workingDirectory = std::move(WorkingDirectory);
37 +}
38 +
39 +void WSLCProcessLauncher::SetDetachKeys(std::string&& DetachKeys)
40 +{
41 + m_detachKeys = std::move(DetachKeys);
42 +}
43 +
44 +void WSLCProcessLauncher::SetUser(std::string&& User)
45 +{
46 + m_user = std::move(User);
47 +}
48 +
49 +std::tuple<WSLCProcessOptions, std::vector<const char*>, std::vector<const char*>> WSLCProcessLauncher::CreateProcessOptions()
50 +{
51 + std::vector<const char*> commandLine;
52 + std::ranges::transform(m_arguments, std::back_inserter(commandLine), [](const std::string& e) { return e.c_str(); });
53 +
54 + std::vector<const char*> environment;
55 + std::ranges::transform(m_environment, std::back_inserter(environment), [](const std::string& e) { return e.c_str(); });
56 +
57 + WSLCProcessOptions options{};
58 + options.CommandLine = {.Values = commandLine.data(), .Count = static_cast<DWORD>(commandLine.size())};
59 + options.Environment = {.Values = environment.data(), .Count = static_cast<DWORD>(environment.size())};
60 + options.TtyColumns = m_columns;
61 + options.TtyRows = m_rows;
62 + options.Flags = m_flags;
63 +
64 + if (!m_workingDirectory.empty())
65 + {
66 + options.CurrentDirectory = m_workingDirectory.c_str();
67 + }
68 +
69 + if (!m_user.empty())
70 + {
71 + options.User = m_user.c_str();
72 + }
73 +
74 + return std::make_tuple(options, std::move(commandLine), std::move(environment));
75 +}
76 +
77 +RunningWSLCProcess::RunningWSLCProcess(WSLCProcessFlags Flags) : m_flags(Flags)
78 +{
79 +}
80 +
81 +WSLCProcessFlags RunningWSLCProcess::Flags() const
82 +{
83 + return m_flags;
84 +}
85 +
86 +int RunningWSLCProcess::GetExitCode()
87 +{
88 + WSLCProcessState state{};
89 + int code{};
90 + GetState(&state, &code);
91 +
92 + THROW_HR_IF_MSG(
93 + HRESULT_FROM_WIN32(ERROR_INVALID_STATE),
94 + state != WslcProcessStateSignalled && state != WslcProcessStateExited,
95 + "Process is not exited. State: %i",
96 + state);
97 +
98 + return code;
99 +}
100 +
101 +WSLCProcessState RunningWSLCProcess::State()
102 +{
103 + WSLCProcessState state{};
104 + int code{};
105 + GetState(&state, &code);
106 +
107 + return state;
108 +}
109 +
110 +std::string WSLCProcessLauncher::FormatResult(const RunningWSLCProcess::ProcessResult& result)
111 +{
112 + auto stdOut = result.Output.find(1);
113 + auto stdErr = result.Output.find(2);
114 +
115 + return std::format(
116 + "{} [{}] exited with: {}. Stdout: '{}', Stderr: '{}'",
117 + m_executable,
118 + wsl::shared::string::Join(m_arguments, ','),
119 + result.Code,
120 + stdOut != result.Output.end() ? stdOut->second : "<none>",
121 + stdErr != result.Output.end() ? stdErr->second : "<none>");
122 +}
123 +
124 +std::string WSLCProcessLauncher::FormatResult(const int code)
125 +{
126 + return std::format("{} [{}] exited with: {}.", m_executable, wsl::shared::string::Join(m_arguments, ','), code);
127 +}
128 +
129 +int RunningWSLCProcess::Wait(DWORD TimeoutMs)
130 +{
131 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_TIMEOUT), !GetExitEvent().wait(TimeoutMs));
132 + return GetExitCode();
133 +}
134 +
135 +RunningWSLCProcess::ProcessResult RunningWSLCProcess::WaitAndCaptureOutput(DWORD TimeoutMs, std::vector<std::unique_ptr<relay::OverlappedIOHandle>>&& ExtraHandles)
136 +{
137 + RunningWSLCProcess::ProcessResult result;
138 +
139 + relay::MultiHandleWait io;
140 +
141 + // Add a callback on IO for each std handle.
142 +
143 + auto addHandle = [&](int fd) {
144 + result.Output.emplace(fd, std::string{});
145 +
146 + auto stdHandle = GetStdHandle(fd);
147 + auto ioCallback = [Index = fd, &result](const gsl::span<char>& Content) {
148 + result.Output[Index].insert(result.Output[Index].end(), Content.begin(), Content.end());
149 + };
150 +
151 + io.AddHandle(std::make_unique<relay::ReadHandle>(std::move(stdHandle), std::move(ioCallback)));
152 + };
153 +
154 + if (WI_IsFlagSet(m_flags, WSLCProcessFlagsTty))
155 + {
156 + addHandle(WSLCFDTty);
157 + }
158 + else
159 + {
160 + addHandle(WSLCFDStdout);
161 + addHandle(WSLCFDStderr);
162 + }
163 +
164 + for (auto& e : ExtraHandles)
165 + {
166 + io.AddHandle(std::move(e));
167 + }
168 +
169 + // Add a callback for when the process exits.
170 + auto exitCallback = [&]() { result.Code = GetExitCode(); };
171 +
172 + io.AddHandle(std::make_unique<relay::EventHandle>(GetExitEvent(), std::move(exitCallback)));
173 +
174 + io.Run(std::chrono::milliseconds(TimeoutMs));
175 +
176 + return result;
177 +}
178 +
179 +std::tuple<HRESULT, std::optional<ClientRunningWSLCProcess>, int> WSLCProcessLauncher::LaunchNoThrow(IWSLCSession& Session)
180 +{
181 + auto [options, commandLine, env] = CreateProcessOptions();
182 +
183 + wil::com_ptr<IWSLCProcess> process;
184 + int error = -1;
185 + auto result = Session.CreateRootNamespaceProcess(m_executable.c_str(), &options, &process, &error);
186 + if (FAILED(result))
187 + {
188 + return std::make_tuple(result, std::optional<ClientRunningWSLCProcess>(), error);
189 + }
190 +
191 + wsl::windows::common::security::ConfigureForCOMImpersonation(process.get());
192 +
193 + return {S_OK, ClientRunningWSLCProcess{std::move(process), m_flags}, 0};
194 +}
195 +
196 +std::tuple<HRESULT, std::optional<ClientRunningWSLCProcess>> WSLCProcessLauncher::LaunchNoThrow(IWSLCContainer& Container)
197 +{
198 + auto [options, commandLine, env] = CreateProcessOptions();
199 +
200 + wil::com_ptr<IWSLCProcess> process;
201 + auto result = Container.Exec(&options, m_detachKeys.has_value() ? m_detachKeys->c_str() : nullptr, &process);
202 + if (FAILED(result))
203 + {
204 + return std::make_pair(result, std::optional<ClientRunningWSLCProcess>());
205 + }
206 +
207 + wsl::windows::common::security::ConfigureForCOMImpersonation(process.get());
208 +
209 + return {S_OK, ClientRunningWSLCProcess{std::move(process), m_flags}};
210 +}
211 +
212 +IWSLCProcess& ClientRunningWSLCProcess::Get()
213 +{
214 + return *m_process.get();
215 +}
216 +
217 +ClientRunningWSLCProcess::ClientRunningWSLCProcess(wil::com_ptr<IWSLCProcess>&& process, WSLCProcessFlags Flags) :
218 + RunningWSLCProcess(Flags), m_process(std::move(process))
219 +{
220 +}
221 +
222 +wil::unique_handle ClientRunningWSLCProcess::GetStdHandle(int Index)
223 +{
224 + wslutil::COMOutputHandle handle;
225 + THROW_IF_FAILED_MSG(m_process->GetStdHandle(static_cast<WSLCFD>(Index), &handle), "Failed to get handle: %i", Index);
226 +
227 + return handle.Release();
228 +}
229 +
230 +wil::unique_event ClientRunningWSLCProcess::GetExitEvent()
231 +{
232 + wil::unique_event event{};
233 + THROW_IF_FAILED(m_process->GetExitEvent(&event));
234 +
235 + return event;
236 +}
237 +
238 +void ClientRunningWSLCProcess::GetState(WSLCProcessState* State, int* Code)
239 +{
240 + THROW_IF_FAILED(m_process->GetState(State, Code));
241 +}
src/windows/common/WSLCProcessLauncher.h new
+116
@@ -0,0 +1,116 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCProcessLauncher.h
8 +
9 +Abstract:
10 +
11 + Helper class to launch and wait for WSLC processes.
12 + This is designed to function both for VM level and container level processes.
13 + This class is also designed to work both from client & server side.
14 +
15 +--*/
16 +
17 +#pragma once
18 +#include "wslc.h"
19 +#include <variant>
20 +#include <vector>
21 +#include <string>
22 +
23 +namespace wsl::windows::common {
24 +
25 +class RunningWSLCProcess
26 +{
27 +public:
28 + struct ProcessResult
29 + {
30 + int Code;
31 + std::map<int, std::string> Output;
32 + };
33 +
34 + RunningWSLCProcess(WSLCProcessFlags Flags);
35 + NON_COPYABLE(RunningWSLCProcess);
36 + DEFAULT_MOVABLE(RunningWSLCProcess);
37 +
38 + ProcessResult WaitAndCaptureOutput(DWORD TimeoutMs = INFINITE, std::vector<std::unique_ptr<relay::OverlappedIOHandle>>&& ExtraHandles = {});
39 + int Wait(DWORD TimeoutMs = INFINITE);
40 + virtual wil::unique_handle GetStdHandle(int Index) = 0;
41 + virtual wil::unique_event GetExitEvent() = 0;
42 + int GetExitCode();
43 + WSLCProcessState State();
44 +
45 + WSLCProcessFlags Flags() const;
46 +
47 +protected:
48 + virtual void GetState(WSLCProcessState* State, int* Code) = 0;
49 +
50 + WSLCProcessFlags m_flags{};
51 +};
52 +
53 +class ClientRunningWSLCProcess : public RunningWSLCProcess
54 +{
55 +public:
56 + NON_COPYABLE(ClientRunningWSLCProcess);
57 + DEFAULT_MOVABLE(ClientRunningWSLCProcess);
58 +
59 + ClientRunningWSLCProcess(wil::com_ptr<IWSLCProcess>&& process, WSLCProcessFlags Flags);
60 + wil::unique_handle GetStdHandle(int Index) override;
61 + wil::unique_event GetExitEvent() override;
62 + IWSLCProcess& Get();
63 +
64 +protected:
65 + void GetState(WSLCProcessState* State, int* Code) override;
66 +
67 +private:
68 + wil::com_ptr<IWSLCProcess> m_process;
69 +};
70 +class WSLCProcessLauncher
71 +{
72 +public:
73 + NON_COPYABLE(WSLCProcessLauncher);
74 + NON_MOVABLE(WSLCProcessLauncher);
75 +
76 + WSLCProcessLauncher(
77 + const std::string& Executable,
78 + const std::vector<std::string>& Arguments,
79 + const std::vector<std::string>& Environment = {},
80 + WSLCProcessFlags = WSLCProcessFlagsNone);
81 +
82 + void SetTtySize(ULONG Rows, ULONG Columns);
83 + void SetWorkingDirectory(std::string&& WorkingDirectory);
84 + void SetUser(std::string&& User);
85 + void SetDetachKeys(std::string&& DetachKeys);
86 +
87 + std::tuple<HRESULT, std::optional<ClientRunningWSLCProcess>, int> LaunchNoThrow(IWSLCSession& Session);
88 + std::tuple<HRESULT, std::optional<ClientRunningWSLCProcess>> LaunchNoThrow(IWSLCContainer& Container);
89 +
90 + template <typename T>
91 + auto Launch(T& Context)
92 + {
93 + auto result = LaunchNoThrow(Context);
94 + THROW_IF_FAILED(std::get<0>(result));
95 +
96 + return std::move(std::get<1>(result).value());
97 + }
98 +
99 + std::string FormatResult(const RunningWSLCProcess::ProcessResult& result);
100 + std::string FormatResult(const int code);
101 +
102 +protected:
103 + std::tuple<WSLCProcessOptions, std::vector<const char*>, std::vector<const char*>> CreateProcessOptions();
104 +
105 + WSLCProcessFlags m_flags{};
106 + std::string m_executable;
107 + std::string m_workingDirectory;
108 + std::string m_user;
109 + std::optional<std::string> m_detachKeys;
110 + std::vector<std::string> m_arguments;
111 + std::vector<std::string> m_environment;
112 + DWORD m_rows = 0;
113 + DWORD m_columns = 0;
114 +};
115 +
116 +} // namespace wsl::windows::common
\ No newline at end of file
src/windows/common/WSLCSessionDefaults.h new
+25
@@ -0,0 +1,25 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionDefaults.h
8 +
9 +Abstract:
10 +
11 + Shared constants for WSLc session naming and storage.
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include <cstdint>
17 +
18 +namespace wsl::windows::wslc {
19 +
20 +inline constexpr const wchar_t DefaultSessionName[] = L"wslc-cli";
21 +inline constexpr const wchar_t DefaultAdminSessionName[] = L"wslc-cli-admin";
22 +inline constexpr const wchar_t DefaultStorageSubPath[] = L"wslc\\sessions";
23 +inline constexpr uint32_t DefaultBootTimeoutMs = 30000;
24 +
25 +} // namespace wsl::windows::wslc
src/windows/common/WSLCUserSettings.cpp new
+411
@@ -0,0 +1,411 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCUserSettings.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of UserSettings — YAML loading and validation.
12 +
13 +--*/
14 +#include "precomp.h"
15 +#include "WSLCUserSettings.h"
16 +#include "filesystem.hpp"
17 +#include "string.hpp"
18 +#include "wslutil.h"
19 +
20 +#pragma warning(push)
21 +#pragma warning(disable : 4251 4275)
22 +#include <yaml-cpp/yaml.h>
23 +#pragma warning(pop)
24 +#include <algorithm>
25 +#include <format>
26 +#include <fstream>
27 +#include <set>
28 +
29 +using namespace wsl::windows::common::string;
30 +
31 +namespace wsl::windows::wslc::settings {
32 +
33 +// All entries are commented out; the values shown are the built-in defaults.
34 +// TODO: localization for comments needed?
35 +static constexpr std::string_view s_DefaultSettingsTemplate =
36 + "# wslc user settings\n"
37 + "# https://aka.ms/wslc-settings\n"
38 + "# All settings support string value \"default\" which uses built-in defaults.\n"
39 + "\n"
40 + "session:\n"
41 + " # Number of virtual CPUs allocated to the session (e.g. 4 default: all available CPUs)\n"
42 + " # cpuCount: default\n"
43 + "\n"
44 + " # Memory limit for the session (e.g. 2GB default: half of available memory)\n"
45 + " # memorySize: default\n"
46 + "\n"
47 + " # Maximum disk image size (e.g. 500GB default: 1TB)\n"
48 + " # maxStorageSize: default\n"
49 + "\n"
50 + "# Credential storage backend: \"wincred\" or \"file\" (default: wincred)\n"
51 + "# credentialStore: wincred\n";
52 +
53 +// Validate individual setting specializations
54 +namespace details {
55 +
56 + std::optional<uint32_t> ParseSettingsMemoryValue(const std::string& value)
57 + {
58 + auto parsed = wsl::shared::string::ParseMemorySize(value.c_str());
59 + auto converted = parsed.has_value() ? *parsed / _1MB : 0; // To Mb, and anything less than 1Mb is considered invalid.
60 + return converted > 0 ? std::optional{static_cast<uint32_t>(converted)} : std::nullopt;
61 + }
62 +
63 +#define WSLC_VALIDATE_SETTING(_setting_) \
64 + std::optional<SettingMapping<Setting::_setting_>::value_t> SettingMapping<Setting::_setting_>::Validate( \
65 + const SettingMapping<Setting::_setting_>::yaml_t& value)
66 +
67 + WSLC_VALIDATE_SETTING(SessionCpuCount)
68 + {
69 + return value > 0 ? std::optional{value} : std::nullopt;
70 + }
71 +
72 + WSLC_VALIDATE_SETTING(SessionMemoryMb)
73 + {
74 + return ParseSettingsMemoryValue(value);
75 + }
76 +
77 + WSLC_VALIDATE_SETTING(SessionStorageSizeMb)
78 + {
79 + return ParseSettingsMemoryValue(value);
80 + }
81 +
82 + WSLC_VALIDATE_SETTING(SessionNetworkingMode)
83 + {
84 + if (value == "none")
85 + {
86 + return WSLCNetworkingModeNone;
87 + }
88 + if (value == "nat")
89 + {
90 + return WSLCNetworkingModeNAT;
91 + }
92 + if (value == "virtioproxy")
93 + {
94 + return WSLCNetworkingModeVirtioProxy;
95 + }
96 +
97 + return std::nullopt;
98 + }
99 +
100 + WSLC_VALIDATE_SETTING(SessionHostFileShareMode)
101 + {
102 + if (value == "plan9")
103 + {
104 + return HostFileShareMode::Plan9;
105 + }
106 + if (value == "virtiofs")
107 + {
108 + return HostFileShareMode::VirtioFs;
109 + }
110 +
111 + return std::nullopt;
112 + }
113 +
114 + WSLC_VALIDATE_SETTING(SessionDnsTunneling)
115 + {
116 + return value;
117 + }
118 +
119 + WSLC_VALIDATE_SETTING(CredentialStore)
120 + {
121 + if (value == "wincred")
122 + {
123 + return CredentialStoreType::WinCred;
124 + }
125 + if (value == "file")
126 + {
127 + return CredentialStoreType::File;
128 + }
129 +
130 + return std::nullopt;
131 + }
132 +
133 +#undef WSLC_VALIDATE_SETTING
134 +
135 +} // namespace details
136 +
137 +// Helpers
138 +namespace {
139 +
140 + // Traverses a dot-separated path (e.g. "session.cpuCount") through a YAML node tree.
141 + // Returns nullopt if any segment is invalid or missing.
142 + std::optional<YAML::Node> NavigateYamlPath(const YAML::Node& root, std::string_view path)
143 + {
144 + YAML::Node current = root;
145 + auto subPaths = wsl::shared::string::Split(std::string{path}, '.');
146 + for (auto const& subPath : subPaths)
147 + {
148 + if (current.IsDefined() && current.IsMap())
149 + {
150 + // Use the const operator[] to avoid yaml-cpp's AssignNode/set_ref side-effect,
151 + // which mutates the shared detail::node and corrupts subsequent lookups.
152 + // Then use reset() to rebind 'current' without triggering set_ref.
153 + auto child = static_cast<const YAML::Node&>(current)[subPath];
154 + if (!child.IsDefined())
155 + {
156 + return std::nullopt;
157 + }
158 + current.reset(child);
159 + }
160 + else
161 + {
162 + return std::nullopt;
163 + }
164 + }
165 + return current;
166 + }
167 +
168 + // Validates and stores a single setting from the YAML document.
169 + template <Setting S>
170 + void ValidateSetting(const YAML::Node& root, SettingsMap& map, const std::wstring& filePath, std::vector<Warning>& warnings)
171 + {
172 + constexpr auto path = details::SettingMapping<S>::YamlPath;
173 + auto node = NavigateYamlPath(root, path);
174 +
175 + if (!node || !node->IsDefined() || node->IsNull())
176 + {
177 + // Key absent — silently use the built-in default.
178 + return;
179 + }
180 +
181 + // Check "default"
182 + try
183 + {
184 + if (node->IsScalar() && node->as<std::string>() == "default")
185 + {
186 + return;
187 + }
188 + }
189 + catch (...)
190 + {
191 + }
192 +
193 + try
194 + {
195 + auto rawValue = node->as<typename details::SettingMapping<S>::yaml_t>();
196 + auto validated = details::SettingMapping<S>::Validate(rawValue);
197 + if (validated.has_value())
198 + {
199 + map.Add<S>(std::move(validated.value()));
200 + }
201 + else
202 + {
203 + const auto widePath = MultiByteToWide(path);
204 + warnings.push_back(
205 + {wsl::shared::Localization::WSLCUserSettings_Warning_InvalidValue(widePath, filePath, node->Mark().line + 1), widePath});
206 + }
207 + }
208 + catch (...)
209 + {
210 + const auto widePath = MultiByteToWide(path);
211 + warnings.push_back(
212 + {wsl::shared::Localization::WSLCUserSettings_Warning_InvalidType(widePath, filePath, node->Mark().line + 1), widePath});
213 + }
214 + }
215 +
216 + // Validates all settings via a fold over the Setting enum index sequence.
217 + template <size_t... S>
218 + void ValidateAll(const YAML::Node& root, SettingsMap& map, const std::wstring& filePath, std::vector<Warning>& warnings, std::index_sequence<S...>)
219 + {
220 + (ValidateSetting<static_cast<Setting>(S)>(root, map, filePath, warnings), ...);
221 + }
222 +
223 + // Collects the set of known dot-separated YAML paths from all SettingMapping specializations.
224 + template <size_t... S>
225 + std::set<std::string> CollectKnownPaths(std::index_sequence<S...>)
226 + {
227 + std::set<std::string> paths;
228 + (paths.insert(std::string(details::SettingMapping<static_cast<Setting>(S)>::YamlPath)), ...);
229 + return paths;
230 + }
231 +
232 + // Derives the set of all prefixes from the known paths.
233 + // e.g. "a.b.c" contributes both "a" and "a.b" as known prefixes.
234 + std::set<std::string> CollectKnownPrefixes(const std::set<std::string>& knownPaths)
235 + {
236 + std::set<std::string> prefixes;
237 + for (const auto& path : knownPaths)
238 + {
239 + for (size_t pos = path.find('.'); pos != std::string::npos; pos = path.find('.', pos + 1))
240 + {
241 + prefixes.insert(path.substr(0, pos));
242 + }
243 + }
244 + return prefixes;
245 + }
246 +
247 + // Iteratively walks the YAML tree and warns about keys not in the known set.
248 + void WarnUnknownKeys(
249 + const YAML::Node& root,
250 + const std::set<std::string>& knownPaths,
251 + const std::set<std::string>& knownPrefixes,
252 + const std::wstring& filePath,
253 + std::vector<Warning>& warnings)
254 + {
255 + // Stack of (node, prefix) pairs to process.
256 + std::vector<std::pair<YAML::Node, std::string>> stack;
257 + stack.emplace_back(root, std::string{});
258 +
259 + while (!stack.empty())
260 + {
261 + auto [node, prefix] = std::move(stack.back());
262 + stack.pop_back();
263 +
264 + for (auto it = node.begin(); it != node.end(); ++it)
265 + {
266 + std::string key;
267 + try
268 + {
269 + key = it->first.as<std::string>();
270 + }
271 + catch (...)
272 + {
273 + auto location = prefix.empty() ? std::wstring(L"root") : MultiByteToWide(prefix);
274 + warnings.push_back(
275 + {wsl::shared::Localization::WSLCUserSettings_Warning_NonStringKey(location, filePath, it->first.Mark().line + 1), location});
276 + continue;
277 + }
278 +
279 + auto fullPath = prefix.empty() ? key : prefix + '.' + key;
280 +
281 + if (it->second.IsMap())
282 + {
283 + if (knownPrefixes.count(fullPath))
284 + {
285 + // Known section — add to stack to traverse.
286 + stack.emplace_back(it->second, fullPath);
287 + }
288 + else
289 + {
290 + // Unknown section — warn once, don't traverse.
291 + const auto widePath = MultiByteToWide(fullPath);
292 + warnings.push_back(
293 + {wsl::shared::Localization::WSLCUserSettings_Warning_UnknownSection(
294 + widePath, filePath, it->first.Mark().line + 1),
295 + widePath});
296 + }
297 + }
298 + else if (!knownPaths.count(fullPath) && !knownPrefixes.count(fullPath))
299 + {
300 + // Unknown setting
301 + const auto widePath = MultiByteToWide(fullPath);
302 + warnings.push_back(
303 + {wsl::shared::Localization::WSLCUserSettings_Warning_UnknownKey(widePath, filePath, it->first.Mark().line + 1), widePath});
304 + }
305 + }
306 + }
307 + }
308 +
309 + // Attempts to parse a YAML document from the given file path.
310 + // Returns an empty optional and pushes a warning if the file exists but fails to parse.
311 + std::optional<YAML::Node> TryLoadYaml(const std::filesystem::path& path, std::vector<Warning>& warnings)
312 + {
313 + std::ifstream stream(path);
314 + if (!stream.is_open())
315 + {
316 + auto err = errno;
317 + // If the file exists but cannot be opened (permissions, sharing violation, etc.),
318 + // emit a warning so the user understands why settings were ignored.
319 + if (err != ENOENT)
320 + {
321 + warnings.push_back({wsl::shared::Localization::WSLCUserSettings_Warning_FailedToOpen(path.wstring(), err), {}});
322 + }
323 +
324 + return std::nullopt;
325 + }
326 +
327 + try
328 + {
329 + return YAML::Load(stream);
330 + }
331 + catch (const std::exception& e)
332 + {
333 + warnings.push_back(
334 + {wsl::shared::Localization::WSLCUserSettings_Warning_ParseError(path.wstring(), MultiByteToWide(e.what())), {}});
335 + return std::nullopt;
336 + }
337 + }
338 +
339 + const std::filesystem::path& SettingsDir()
340 + {
341 + static const std::filesystem::path dir = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc";
342 + return dir;
343 + }
344 +} // namespace
345 +
346 +UserSettings const& UserSettings::Instance()
347 +{
348 + static UserSettings instance;
349 + return instance;
350 +}
351 +
352 +UserSettings::UserSettings() : UserSettings(SettingsDir())
353 +{
354 +}
355 +
356 +UserSettings::UserSettings(const std::filesystem::path& settingsDir)
357 +{
358 + m_settingsPath = settingsDir / L"settings.yaml";
359 +
360 + auto root = TryLoadYaml(m_settingsPath, m_warnings);
361 + if (root.has_value())
362 + {
363 + m_type = UserSettingsType::Standard;
364 + const auto filePath = m_settingsPath.wstring();
365 +
366 + if (root->IsMap())
367 + {
368 + constexpr auto settingCount = static_cast<size_t>(Setting::Max);
369 + ValidateAll(root.value(), m_settings, filePath, m_warnings, std::make_index_sequence<settingCount>());
370 +
371 + constexpr auto indexSeq = std::make_index_sequence<settingCount>();
372 + auto knownPaths = CollectKnownPaths(indexSeq);
373 + auto knownPrefixes = CollectKnownPrefixes(knownPaths);
374 + WarnUnknownKeys(root.value(), knownPaths, knownPrefixes, filePath, m_warnings);
375 + }
376 + else
377 + {
378 + m_warnings.push_back({wsl::shared::Localization::WSLCUserSettings_Warning_InvalidStructure(filePath), {}});
379 + }
380 + }
381 +
382 + // Emit any settings load warnings.
383 + for (const auto& warning : m_warnings)
384 + {
385 + wsl::windows::common::wslutil::PrintMessage(warning.Message, stderr);
386 + }
387 +}
388 +
389 +void UserSettings::Reset() const
390 +{
391 + std::filesystem::create_directories(m_settingsPath.parent_path());
392 + std::ofstream file(m_settingsPath);
393 + THROW_HR_IF_MSG(E_UNEXPECTED, !file.is_open(), "Failed to create settings file");
394 + file << s_DefaultSettingsTemplate;
395 +}
396 +
397 +void UserSettings::PrepareToShellExecuteFile() const
398 +{
399 + if (m_type == UserSettingsType::Default && !std::filesystem::exists(m_settingsPath))
400 + {
401 + // First run — create the directory and write the commented-out defaults template.
402 + Reset();
403 + }
404 +}
405 +
406 +std::filesystem::path UserSettings::SettingsFilePath() const
407 +{
408 + return m_settingsPath;
409 +}
410 +
411 +} // namespace wsl::windows::wslc::settings
src/windows/common/WSLCUserSettings.h new
+185
@@ -0,0 +1,185 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCUserSettings.h
8 +
9 +Abstract:
10 +
11 + Declaration of UserSettings — the singleton that loads, validates, and
12 + provides access to the wslc user settings file.
13 +
14 +--*/
15 +#pragma once
16 +#include "defs.h"
17 +#include "EnumVariantMap.h"
18 +#include "wslc.h"
19 +#include <cstdint>
20 +#include <filesystem>
21 +#include <optional>
22 +#include <string>
23 +#include <string_view>
24 +#include <vector>
25 +
26 +// How to add a setting:
27 +// 1 - Add an entry to the Setting enum.
28 +// 2 - Add a DEFINE_SETTING_MAPPING specialization with yaml_t, value_t, default, and YAML path.
29 +// 3 - Implement the Validate function in UserSettings.cpp if needed, otherwise use pass through.
30 +
31 +namespace wsl::windows::wslc::settings {
32 +
33 +// Enum of all user settings.
34 +// Must start at 0 to enable direct variant indexing.
35 +// Max must be last and unused.
36 +enum class Setting : size_t
37 +{
38 + SessionCpuCount = 0,
39 + SessionMemoryMb,
40 + SessionStorageSizeMb,
41 + SessionNetworkingMode,
42 + SessionHostFileShareMode,
43 + SessionDnsTunneling,
44 + CredentialStore,
45 +
46 + Max
47 +};
48 +
49 +enum class HostFileShareMode
50 +{
51 + Plan9,
52 + VirtioFs
53 +};
54 +
55 +enum class CredentialStoreType
56 +{
57 + WinCred,
58 + File
59 +};
60 +
61 +namespace details {
62 +
63 + template <Setting S>
64 + struct SettingMapping
65 + {
66 + // yaml_t - the C++ type read from the YAML node via node.as<yaml_t>()
67 + // value_t - the native type stored in SettingsMap
68 + // DefaultValue - used when the key is absent or fails validation
69 + // YamlPath - dot-separated path into the YAML document (e.g. "session.cpuCount")
70 + // Validate - semantic validation; returns nullopt to reject and fall back to default
71 + };
72 +
73 + // clang-format off
74 +#define DEFINE_SETTING_MAPPING(_setting_, _yaml_t_, _value_t_, _default_, _path_) \
75 + template <> \
76 + struct SettingMapping<Setting::_setting_> \
77 + { \
78 + using yaml_t = _yaml_t_; \
79 + using value_t = _value_t_; \
80 + inline static const value_t DefaultValue = _default_; \
81 + static constexpr std::string_view YamlPath = _path_; \
82 + static std::optional<value_t> Validate(const yaml_t& value); \
83 + };
84 +
85 + DEFINE_SETTING_MAPPING(SessionCpuCount, uint32_t, uint32_t, 0, "session.cpuCount")
86 + DEFINE_SETTING_MAPPING(SessionMemoryMb, std::string, uint32_t, 0, "session.memorySize")
87 + DEFINE_SETTING_MAPPING(SessionStorageSizeMb, std::string, uint32_t, 1048576, "session.maxStorageSize")
88 + DEFINE_SETTING_MAPPING(SessionNetworkingMode, std::string, WSLCNetworkingMode, WSLCNetworkingModeVirtioProxy, "session.networkingMode")
89 + DEFINE_SETTING_MAPPING(SessionHostFileShareMode, std::string, HostFileShareMode, HostFileShareMode::VirtioFs, "session.hostFileShareMode")
90 + DEFINE_SETTING_MAPPING(SessionDnsTunneling, bool, bool, true, "session.dnsTunneling")
91 + DEFINE_SETTING_MAPPING(CredentialStore, std::string, CredentialStoreType, CredentialStoreType::WinCred, "credentialStore")
92 +
93 +#undef DEFINE_SETTING_MAPPING
94 + // clang-format on
95 +
96 +} // namespace details
97 +
98 +// Type-safe enum-indexed map of all settings values, backed by EnumBasedVariantMap.
99 +struct SettingsMap : wsl::windows::wslc::EnumBasedVariantMap<Setting, details::SettingMapping>
100 +{
101 + // Returns the stored value if present, otherwise the compile-time default.
102 + template <Setting S>
103 + typename details::SettingMapping<S>::value_t GetOrDefault() const
104 + {
105 + if (Contains(S))
106 + {
107 + return Get<S>();
108 + }
109 + return details::SettingMapping<S>::DefaultValue;
110 + }
111 +};
112 +
113 +// Indicates which source the settings were loaded from.
114 +enum class UserSettingsType
115 +{
116 + Default, // Settings file did not exist or failed to parse; built-in defaults are used.
117 + Standard, // Settings file (settings.yaml) loaded successfully.
118 +};
119 +
120 +struct Warning
121 +{
122 + std::wstring Message;
123 + std::wstring SettingPath; // Empty for file-level warnings; key path for per-field warnings.
124 +};
125 +
126 +// Singleton that owns the parsed settings for the current process lifetime.
127 +// Load order:
128 +// 1. settings.yaml (Standard)
129 +// 2. Built-in defaults (Default, if the file is absent or fails to parse)
130 +class UserSettings
131 +{
132 +public:
133 + // Returns the singleton instance. Loaded on first call; subsequent calls are no-ops.
134 + static UserSettings const& Instance();
135 +
136 + NON_COPYABLE(UserSettings);
137 + NON_MOVABLE(UserSettings);
138 +
139 + // Returns the value for setting S, or its built-in default if not present in the file.
140 + template <Setting S>
141 + typename details::SettingMapping<S>::value_t Get() const
142 + {
143 + return m_settings.GetOrDefault<S>();
144 + }
145 +
146 + std::vector<Warning> const& GetWarnings() const
147 + {
148 + return m_warnings;
149 + }
150 +
151 + UserSettingsType GetType() const
152 + {
153 + return m_type;
154 + }
155 +
156 + // Called before opening the settings file in an editor.
157 + // If type is Default, creates the file from the commented-out defaults template.
158 + void PrepareToShellExecuteFile() const;
159 +
160 + std::filesystem::path SettingsFilePath() const;
161 +
162 + // Overwrites the settings file with the commented-out defaults template.
163 + void Reset() const;
164 +
165 + // Loads settings from an explicit directory.
166 + explicit UserSettings(const std::filesystem::path& settingsDir);
167 + ~UserSettings() = default;
168 +
169 +private:
170 + UserSettings();
171 +
172 + SettingsMap m_settings;
173 + std::vector<Warning> m_warnings;
174 + UserSettingsType m_type = UserSettingsType::Default;
175 + std::filesystem::path m_settingsPath;
176 +};
177 +
178 +// Convenience free function — returns the singleton instance.
179 +// Usage: settings::User().Get<Setting::Foo>()
180 +inline UserSettings const& User()
181 +{
182 + return UserSettings::Instance();
183 +}
184 +
185 +} // namespace wsl::windows::wslc::settings
src/windows/common/WindowsUpdateIntegration.cpp new
+360
@@ -0,0 +1,360 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WindowsUpdateIntegration.cpp
8 +
9 +Abstract:
10 +
11 + This file contains objects related to invoking the Windows Update Agent API.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "WindowsUpdateIntegration.h"
17 +
18 +namespace wsl::windows::common {
19 +
20 +namespace anon {
21 + struct DefaultWindowsUpdateClassFactory : public WindowsUpdateClassFactory
22 + {
23 + wil::com_ptr<IUpdateSession> CreateUpdateSession() const override
24 + {
25 + wil::com_ptr<IUpdateSession> result;
26 + THROW_IF_FAILED(CoCreateInstance(CLSID_UpdateSession, nullptr, CLSCTX_INPROC_SERVER, IID_IUpdateSession, (void**)&result));
27 + return result;
28 + }
29 +
30 + wil::com_ptr<IUpdateCollection> CreateUpdateCollection() const override
31 + {
32 + wil::com_ptr<IUpdateCollection> result;
33 + THROW_IF_FAILED(CoCreateInstance(CLSID_UpdateCollection, nullptr, CLSCTX_INPROC_SERVER, IID_IUpdateCollection, (void**)&result));
34 + return result;
35 + }
36 + };
37 +
38 + struct DownloadProgressChangedCallback
39 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IDownloadProgressChangedCallback>
40 + {
41 + DownloadProgressChangedCallback(std::function<void(uint32_t)> progress) : m_progress(std::move(progress))
42 + {
43 + }
44 +
45 + IFACEMETHOD(Invoke)(IDownloadJob*, IDownloadProgressChangedCallbackArgs* callbackArgs) override
46 + {
47 + wil::com_ptr<IDownloadProgress> progress;
48 + RETURN_IF_FAILED(callbackArgs->get_Progress(&progress));
49 +
50 + LONG percent{};
51 + RETURN_IF_FAILED(progress->get_PercentComplete(&percent));
52 +
53 + m_progress(static_cast<uint32_t>(percent));
54 + return S_OK;
55 + }
56 +
57 + private:
58 + std::function<void(uint32_t)> m_progress;
59 + };
60 +
61 + struct DownloadCompletedCallback
62 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IDownloadCompletedCallback>
63 + {
64 + DownloadCompletedCallback()
65 + {
66 + }
67 +
68 + IFACEMETHOD(Invoke)(IDownloadJob*, IDownloadCompletedCallbackArgs*) override
69 + {
70 + m_completed.SetEvent();
71 + return S_OK;
72 + }
73 +
74 + void Wait()
75 + {
76 + m_completed.wait();
77 + }
78 +
79 + private:
80 + wil::slim_event_manual_reset m_completed;
81 + };
82 +
83 + struct InstallationProgressChangedCallback
84 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IInstallationProgressChangedCallback>
85 + {
86 + InstallationProgressChangedCallback(std::function<void(uint32_t)> progress) : m_progress(std::move(progress))
87 + {
88 + }
89 +
90 + IFACEMETHOD(Invoke)(IInstallationJob*, IInstallationProgressChangedCallbackArgs* callbackArgs) override
91 + {
92 + wil::com_ptr<IInstallationProgress> progress;
93 + RETURN_IF_FAILED(callbackArgs->get_Progress(&progress));
94 +
95 + LONG percent{};
96 + RETURN_IF_FAILED(progress->get_PercentComplete(&percent));
97 +
98 + m_progress(static_cast<uint32_t>(percent));
99 + return S_OK;
100 + }
101 +
102 + private:
103 + std::function<void(uint32_t)> m_progress;
104 + };
105 +
106 + struct InstallationCompletedCallback
107 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IInstallationCompletedCallback>
108 + {
109 + InstallationCompletedCallback()
110 + {
111 + }
112 +
113 + IFACEMETHOD(Invoke)(IInstallationJob*, IInstallationCompletedCallbackArgs*) override
114 + {
115 + m_completed.SetEvent();
116 + return S_OK;
117 + }
118 +
119 + void Wait()
120 + {
121 + m_completed.wait();
122 + }
123 +
124 + private:
125 + wil::slim_event_manual_reset m_completed;
126 + };
127 +} // namespace anon
128 +
129 +WindowsUpdateContext::WindowsUpdateContext() :
130 + WindowsUpdateContext(std::make_unique<anon::DefaultWindowsUpdateClassFactory>(), WslProductIdentifier())
131 +{
132 +}
133 +
134 +WindowsUpdateContext::WindowsUpdateContext(std::wstring product) :
135 + WindowsUpdateContext(std::make_unique<anon::DefaultWindowsUpdateClassFactory>(), std::move(product))
136 +{
137 +}
138 +
139 +WindowsUpdateContext::WindowsUpdateContext(std::unique_ptr<WindowsUpdateClassFactory> factory, std::wstring product) :
140 + m_factory(std::move(factory)), m_product(std::move(product))
141 +{
142 + m_session = m_factory->CreateUpdateSession();
143 +
144 + auto applicationID = wil::make_bstr(L"Windows Subsystem for Linux");
145 + THROW_IF_FAILED(m_session->put_ClientApplicationID(applicationID.get()));
146 +
147 + m_activity = std::make_unique<ActivityType>();
148 + TraceLoggingWriteStart(
149 + *m_activity,
150 + "WindowsUpdateContext",
151 + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage),
152 + TraceLoggingValue(WSL_PACKAGE_VERSION, "wslVersion"),
153 + TraceLoggingWideString(m_product.c_str(), "product"));
154 +}
155 +
156 +std::wstring WindowsUpdateContext::WslProductIdentifier()
157 +{
158 + return STRING_TO_WIDE_STRING(DCAT_PRODUCT_NAME);
159 +}
160 +
161 +void WindowsUpdateContext::EnsureProductRegistryEntry() const
162 +{
163 + wsl::windows::common::helpers::RegisterWithDcat(false);
164 +}
165 +
166 +size_t WindowsUpdateContext::SearchForUpdates()
167 +{
168 + TraceLoggingWriteTagged(
169 + *m_activity, "SearchForUpdates", TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
170 + THROW_IF_FAILED(m_session->CreateUpdateSearcher(&m_searcher));
171 +
172 + std::wstring queryString = std::format(L"Product='{}'", m_product);
173 + auto queryBSTR = wil::make_bstr(queryString.c_str());
174 +
175 + wil::com_ptr<ISearchResult> searchResult;
176 + THROW_IF_FAILED(m_searcher->Search(queryBSTR.get(), &searchResult));
177 +
178 + OperationResultCode resultCode{};
179 + THROW_IF_FAILED(searchResult->get_ResultCode(&resultCode));
180 +
181 + THROW_HR_IF(WSLC_E_WU_SEARCH_FAILED, resultCode != OperationResultCode::orcSucceeded && resultCode != OperationResultCode::orcSucceededWithErrors);
182 +
183 + if (resultCode == OperationResultCode::orcSucceededWithErrors)
184 + {
185 + wil::com_ptr<IUpdateExceptionCollection> warnings;
186 + if (SUCCEEDED_LOG(searchResult->get_Warnings(&warnings)) && warnings)
187 + {
188 + LONG warningCount{};
189 + if (SUCCEEDED_LOG(warnings->get_Count(&warningCount)))
190 + {
191 + for (LONG i = 0; i < warningCount; ++i)
192 + {
193 + wil::com_ptr<IUpdateException> warning;
194 + if (FAILED_LOG(warnings->get_Item(i, &warning)) || !warning)
195 + {
196 + continue;
197 + }
198 +
199 + wil::unique_bstr message;
200 + LONG hr{};
201 + UpdateExceptionContext context{};
202 + warning->get_Message(&message);
203 + warning->get_HResult(&hr);
204 + warning->get_Context(&context);
205 +
206 + TraceLoggingWriteTagged(
207 + *m_activity,
208 + "SearchWarning",
209 + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
210 + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage),
211 + TraceLoggingHResult(hr, "warningHResult"),
212 + TraceLoggingUInt32(static_cast<uint32_t>(context), "warningContext"),
213 + TraceLoggingWideString(message.get(), "warningMessage"));
214 + }
215 + }
216 + }
217 + }
218 +
219 + THROW_IF_FAILED(searchResult->get_Updates(&m_updates));
220 + return GetUpdateCount();
221 +}
222 +
223 +size_t WindowsUpdateContext::GetUpdateCount() const
224 +{
225 + LONG result{};
226 + if (m_updates)
227 + {
228 + THROW_IF_FAILED(m_updates->get_Count(&result));
229 + }
230 + return static_cast<size_t>(result);
231 +}
232 +
233 +void WindowsUpdateContext::DownloadUpdates(const std::function<void(uint32_t)>& progress) const
234 +{
235 + TraceLoggingWriteTagged(
236 + *m_activity, "DownloadUpdates", TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
237 + // Collect all of the updates that are not currently downloaded
238 + wil::com_ptr<IUpdateCollection> toDownload = m_factory->CreateUpdateCollection();
239 +
240 + for (size_t i = 0, count = GetUpdateCount(); i < count; ++i)
241 + {
242 + wil::com_ptr<IUpdate> update;
243 + THROW_IF_FAILED(m_updates->get_Item(static_cast<LONG>(i), &update));
244 + VARIANT_BOOL downloaded = VARIANT_FALSE;
245 + THROW_IF_FAILED(update->get_IsDownloaded(&downloaded));
246 + if (downloaded == VARIANT_FALSE)
247 + {
248 + THROW_IF_FAILED(toDownload->Add(update.get(), nullptr));
249 + }
250 + }
251 +
252 + // All updates are already downloaded — nothing to do.
253 + LONG toDownloadCount{};
254 + THROW_IF_FAILED(toDownload->get_Count(&toDownloadCount));
255 + if (toDownloadCount == 0)
256 + {
257 + if (progress)
258 + {
259 + progress(100);
260 + }
261 + return;
262 + }
263 +
264 + wil::com_ptr<IUpdateDownloader> updateDownloader;
265 + THROW_IF_FAILED(m_session->CreateUpdateDownloader(&updateDownloader));
266 +
267 + THROW_IF_FAILED(updateDownloader->put_Updates(toDownload.get()));
268 +
269 + Microsoft::WRL::ComPtr<anon::DownloadProgressChangedCallback> downloadProgress;
270 + if (progress)
271 + {
272 + downloadProgress = wil::MakeOrThrow<anon::DownloadProgressChangedCallback>(progress);
273 + }
274 + auto downloadCompleted = wil::MakeOrThrow<anon::DownloadCompletedCallback>();
275 + wil::com_ptr<IDownloadJob> downloadJob;
276 +
277 + THROW_IF_FAILED(updateDownloader->BeginDownload(downloadProgress.Get(), downloadCompleted.Get(), VARIANT{}, &downloadJob));
278 + downloadCompleted->Wait();
279 + THROW_IF_FAILED(downloadJob->CleanUp());
280 +
281 + wil::com_ptr<IDownloadResult> result;
282 + THROW_IF_FAILED(updateDownloader->EndDownload(downloadJob.get(), &result));
283 +
284 + HRESULT downloadHResult{};
285 + THROW_IF_FAILED(result->get_HResult(&downloadHResult));
286 + THROW_IF_FAILED(downloadHResult);
287 +}
288 +
289 +void WindowsUpdateContext::InstallUpdates(const std::function<void(uint32_t)>& progress) const
290 +{
291 + TraceLoggingWriteTagged(
292 + *m_activity, "InstallUpdates", TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
293 + wil::com_ptr<IUpdateInstaller> updateInstaller;
294 + THROW_IF_FAILED(m_session->CreateUpdateInstaller(&updateInstaller));
295 +
296 + THROW_IF_FAILED(updateInstaller->put_Updates(m_updates.get()));
297 +
298 + Microsoft::WRL::ComPtr<anon::InstallationProgressChangedCallback> installationProgress;
299 + if (progress)
300 + {
301 + installationProgress = wil::MakeOrThrow<anon::InstallationProgressChangedCallback>(progress);
302 + }
303 + auto installationCompleted = wil::MakeOrThrow<anon::InstallationCompletedCallback>();
304 + wil::com_ptr<IInstallationJob> installationJob;
305 +
306 + THROW_IF_FAILED(updateInstaller->BeginInstall(installationProgress.Get(), installationCompleted.Get(), VARIANT{}, &installationJob));
307 + installationCompleted->Wait();
308 + THROW_IF_FAILED(installationJob->CleanUp());
309 +
310 + wil::com_ptr<IInstallationResult> result;
311 + THROW_IF_FAILED(updateInstaller->EndInstall(installationJob.get(), &result));
312 +
313 + HRESULT installationHResult{};
314 + THROW_IF_FAILED(result->get_HResult(&installationHResult));
315 + THROW_IF_FAILED(installationHResult);
316 +}
317 +
318 +void WindowsUpdateContext::RunUpdateFlow(bool forceInstall, const std::function<void(uint32_t)>& progress)
319 +{
320 + TraceLoggingWriteTagged(
321 + *m_activity, "RunUpdateFlow", TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
322 +
323 + static_assert(
324 + DownloadProgressPercent + InstallProgressPercent == 100, "Download and Install progress values must add up to 100.");
325 +
326 + if (progress)
327 + {
328 + progress(0);
329 + }
330 +
331 + if (forceInstall)
332 + {
333 + EnsureProductRegistryEntry();
334 + }
335 +
336 + size_t updateCount = SearchForUpdates();
337 + if (!updateCount)
338 + {
339 + if (progress)
340 + {
341 + progress(100);
342 + }
343 + return;
344 + }
345 +
346 + std::function<void(uint32_t)> downloadProgress;
347 + if (progress)
348 + {
349 + downloadProgress = [&](uint32_t percent) { progress((percent * DownloadProgressPercent) / 100); };
350 + }
351 + DownloadUpdates(downloadProgress);
352 +
353 + std::function<void(uint32_t)> installProgress;
354 + if (progress)
355 + {
356 + installProgress = [&](uint32_t percent) { progress(DownloadProgressPercent + ((percent * InstallProgressPercent) / 100)); };
357 + }
358 + InstallUpdates(installProgress);
359 +}
360 +} // namespace wsl::windows::common
src/windows/common/WindowsUpdateIntegration.h new
+84
@@ -0,0 +1,84 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WindowsUpdateIntegration.h
8 +
9 +Abstract:
10 +
11 + This file contains objects related to invoking the Windows Update Agent API.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +namespace wsl::windows::common {
18 +// Class factory for Windows Update Agent objects.
19 +struct WindowsUpdateClassFactory
20 +{
21 + virtual ~WindowsUpdateClassFactory() = default;
22 +
23 + virtual wil::com_ptr<IUpdateSession> CreateUpdateSession() const = 0;
24 +
25 + virtual wil::com_ptr<IUpdateCollection> CreateUpdateCollection() const = 0;
26 +};
27 +
28 +// Holds the context for performing a Windows Update Agent action.
29 +struct WindowsUpdateContext
30 +{
31 + // Create a context using the default class factory and WSL product.
32 + WindowsUpdateContext();
33 +
34 + // Create a context using the default class factory.
35 + WindowsUpdateContext(std::wstring product);
36 +
37 + // Create a context using the provided class factory.
38 + WindowsUpdateContext(std::unique_ptr<WindowsUpdateClassFactory> factory, std::wstring product);
39 +
40 + NON_COPYABLE(WindowsUpdateContext);
41 + DEFAULT_MOVABLE(WindowsUpdateContext);
42 +
43 + // Gets the appropriate product for the currently running WSL instance.
44 + static std::wstring WslProductIdentifier();
45 +
46 + // Ensures that the product is registered in with the Windows Update system.
47 + // This is required to use the system for initial installs.
48 + void EnsureProductRegistryEntry() const;
49 +
50 + // Searches for updates for the product.
51 + // Returns the number of updates found.
52 + size_t SearchForUpdates();
53 +
54 + // Gets the number of updates found by `SearchForUpdates`.
55 + size_t GetUpdateCount() const;
56 +
57 + // Downloads any updates that are not yet downloaded.
58 + // Calls the progress callback, if provided, with the overall download progress estimate.
59 + void DownloadUpdates(const std::function<void(uint32_t)>& progress = {}) const;
60 +
61 + // Installs any updates that were found.
62 + // Calls the progress callback, if provided, with the overall install progress estimate.
63 + void InstallUpdates(const std::function<void(uint32_t)>& progress = {}) const;
64 +
65 + static constexpr uint32_t DownloadProgressPercent = 70;
66 + static constexpr uint32_t InstallProgressPercent = 30;
67 +
68 + // Performs a complete update flow. This is a convenience method to remove the need to call and coordinate the individual actions.
69 + // When `forceInstall` is true, `EnsureProductRegistryEntry` is called.
70 + // Calls the progress callback, if provided, with the overall update progress estimate.
71 + // Download and install phases are split according to the values defined above.
72 + void RunUpdateFlow(bool forceInstall = false, const std::function<void(uint32_t)>& progress = {});
73 +
74 +private:
75 + using ActivityType = TraceLoggingActivity<g_hTraceLoggingProvider, MICROSOFT_KEYWORD_MEASURES>;
76 +
77 + std::unique_ptr<WindowsUpdateClassFactory> m_factory;
78 + std::wstring m_product;
79 + wil::com_ptr<IUpdateSession> m_session;
80 + wil::com_ptr<IUpdateSearcher> m_searcher;
81 + wil::com_ptr<IUpdateCollection> m_updates;
82 + std::unique_ptr<ActivityType> m_activity;
83 +};
84 +} // namespace wsl::windows::common
src/windows/common/WslClient.cpp
+8 -14
@@ -19,6 +19,7 @@ Abstract:
19 #include "Distribution.h"
20 #include "CommandLine.h"
21 #include <conio.h>
22 +#include "WslCoreFilesystem.h"
23
24 #define BASH_PATH L"/bin/bash"
25
@@ -107,17 +108,9 @@ struct ShellExecOptions
108 }
109 };
110
110 -bool IsInteractiveConsole()
111 -{
112 - const HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
113 - DWORD mode{};
114 -
115 - return GetFileType(stdinHandle) == FILE_TYPE_CHAR && GetConsoleMode(stdinHandle, &mode);
116 -}
117 -
111 void PromptForKeyPress()
112 {
120 - if (IsInteractiveConsole())
113 + if (wsl::windows::common::wslutil::IsInteractiveConsole())
114 {
115 wsl::windows::common::wslutil::PrintMessage(wsl::shared::Localization::MessagePressAnyKeyToExit());
116 LOG_IF_WIN32_BOOL_FALSE(FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE)));
@@ -259,7 +252,7 @@ int ExportDistribution(_In_ std::wstring_view commandLine)
252
253 parser.AddPositionalArgument(name, 0);
254 parser.AddPositionalArgument(filePath, 1);
262 - parser.AddArgument(SetFlag<ULONG, LXSS_EXPORT_DISTRO_FLAGS_VHD>(flags), WSL_EXPORT_ARG_VHD_OPTION);
255 + parser.AddArgument(SetFlag<LXSS_EXPORT_DISTRO_FLAGS_VHD, ULONG>(flags), WSL_EXPORT_ARG_VHD_OPTION);
256 parser.AddArgument(parseFormat, WSL_EXPORT_ARG_FORMAT_OPTION);
257 parser.Parse();
258
@@ -325,7 +318,7 @@ int ImportDistribution(_In_ std::wstring_view commandLine)
318 parser.AddPositionalArgument(AbsolutePath(installPath), 1);
319 parser.AddPositionalArgument(filePath, 2);
320 parser.AddArgument(WslVersion(version), WSL_IMPORT_ARG_VERSION);
328 - parser.AddArgument(SetFlag<ULONG, LXSS_IMPORT_DISTRO_FLAGS_VHD>{flags}, WSL_IMPORT_ARG_VHD);
321 + parser.AddArgument(SetFlag<LXSS_IMPORT_DISTRO_FLAGS_VHD, ULONG>{flags}, WSL_IMPORT_ARG_VHD);
322
323 parser.Parse();
324
@@ -1505,10 +1498,11 @@ int RunDebugShell()
1498 THROW_IF_WIN32_BOOL_FALSE(WriteFile(pipe.get(), "\n", 1, nullptr, nullptr));
1499
1500 // Create a thread to relay stdin to the pipe.
1508 - wsl::windows::common::ConsoleState Io;
1501 + wsl::windows::common::ConsoleState console;
1502 auto exitEvent = wil::unique_event(wil::EventOptions::ManualReset);
1510 - std::thread inputThread(
1511 - [&]() { wsl::windows::common::RelayStandardInput(GetStdHandle(STD_INPUT_HANDLE), pipe.get(), {}, exitEvent.get(), &Io); });
1503 + std::thread inputThread([&]() {
1504 + wsl::windows::common::relay::StandardInputRelay(GetStdHandle(STD_INPUT_HANDLE), pipe.get(), []() {}, exitEvent.get());
1505 + });
1506
1507 auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1508 exitEvent.SetEvent();
src/windows/common/WslCoreConfig.cpp
+10 -19
@@ -419,26 +419,14 @@ void wsl::core::Config::Initialize(_In_opt_ HANDLE UserToken)
419 {
420 try
421 {
422 - // Open a handle to the service control manager and check if the inbox service is registered.
423 - const wil::unique_schandle manager{OpenSCManager(nullptr, nullptr, SC_MANAGER_ENUMERATE_SERVICE)};
424 - THROW_LAST_ERROR_IF(!manager);
425 -
426 - // Check if the service is running.
427 - const wil::unique_schandle service{OpenServiceW(manager.get(), L"GlobalSecureAccessTunnelingService", SERVICE_QUERY_STATUS)};
428 - if (service)
422 + if (wsl::windows::common::helpers::IsServiceRunning(L"GlobalSecureAccessTunnelingService"))
423 {
430 - SERVICE_STATUS status;
431 - THROW_IF_WIN32_BOOL_FALSE(QueryServiceStatus(service.get(), &status));
432 -
433 - if (status.dwCurrentState != SERVICE_STOPPED)
424 + if (DnsTunnelingConfigPresence == ConfigKeyPresence::Present)
425 {
435 - if (DnsTunnelingConfigPresence == ConfigKeyPresence::Present)
436 - {
437 - EMIT_USER_WARNING(wsl::shared::Localization::MessageDnsTunnelingDisabled());
438 - }
439 -
440 - EnableDnsTunneling = false;
426 + EMIT_USER_WARNING(wsl::shared::Localization::MessageDnsTunnelingDisabled());
427 }
428 +
429 + EnableDnsTunneling = false;
430 }
431 }
432 CATCH_LOG()
@@ -474,9 +462,12 @@ void wsl::core::Config::Initialize(_In_opt_ HANDLE UserToken)
462 EnableVirtio9p = false;
463 }
464
477 - if (NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored)
465 + if (NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored && NetworkingMode != NetworkingMode::VirtioProxy)
466 {
479 - VALIDATE_CONFIG_OPTION((NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored), EnableDnsTunneling, false);
467 + VALIDATE_CONFIG_OPTION(
468 + (NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored && NetworkingMode != NetworkingMode::VirtioProxy),
469 + EnableDnsTunneling,
470 + false);
471 }
472
473 if (!EnableDnsTunneling)
src/windows/common/WslCoreFilesystem.cpp
+11 -3
@@ -29,8 +29,10 @@ wil::unique_hfile wsl::core::filesystem::CreateFile(
29
30 void wsl::core::filesystem::CreateVhd(_In_ LPCWSTR target, _In_ ULONGLONG maximumSize, _In_ PSID userSid, _In_ BOOL sparse, _In_ BOOL fixed)
31 {
32 - WI_ASSERT(wsl::windows::common::string::IsPathComponentEqual(
33 - std::filesystem::path{target}.extension().native(), windows::common::wslutil::c_vhdxFileExtension));
32 + THROW_HR_IF(
33 + E_INVALIDARG,
34 + !wsl::windows::common::string::IsPathComponentEqual(
35 + std::filesystem::path{target}.extension().native(), windows::common::wslutil::c_vhdxFileExtension));
36
37 // Disable creation of sparse VHDs while data corruption is being debugged.
38 if (sparse)
@@ -64,9 +66,15 @@ void wsl::core::filesystem::CreateVhd(_In_ LPCWSTR target, _In_ ULONGLONG maximu
66 // N.B. This ensures that HcsGrantVmAccess is able to add the required ACL
67 // to the VHD because the operation is done while impersonating the user.
68 auto sd = windows::common::security::CreateSecurityDescriptor(userSid);
69 +
70 wil::unique_hfile vhd{};
68 - THROW_IF_WIN32_ERROR(
71 + auto result = HRESULT_FROM_WIN32(
72 ::CreateVirtualDisk(&storageType, target, VIRTUAL_DISK_ACCESS_NONE, &sd, flags, 0, &createVhdParameters, nullptr, &vhd));
73 + if (FAILED(result))
74 + {
75 + THROW_HR_WITH_USER_ERROR(
76 + result, shared::Localization::MessageFailedToCreateDisk(target, windows::common::wslutil::GetErrorString(result)));
77 + }
78 }
79
80 wil::unique_handle wsl::core::filesystem::OpenVhd(_In_ LPCWSTR Path, _In_ VIRTUAL_DISK_ACCESS_MASK Mask)
src/windows/common/WslCoreHostDnsInfo.cpp
+1 -1
@@ -357,7 +357,7 @@ std::string wsl::core::networking::GenerateResolvConf(_In_ const DnsInfo& Info)
357
358 std::vector<std::string> wsl::core::networking::GetAllDnsSuffixes(const std::vector<IpAdapterAddress>& AdapterAddresses)
359 {
360 - const auto com = wil::CoInitializeEx();
360 + const auto com = InitializeCOMState();
361 wsl::core::WmiService service(L"ROOT\\StandardCimv2");
362
363 // DNS suffixes will be configured in Linux in the following order, *similar* (not 100% the same) to the order in which Windows tries suffixes.
src/windows/common/WslCoreHostDnsInfo.h
+92 -92
@@ -1,93 +1,93 @@
1 -// Copyright (C) Microsoft Corporation. All rights reserved.
2 -
3 -#pragma once
4 -#include <string>
5 -#include <vector>
6 -
7 -#include <iptypes.h>
8 -#include <wil/registry.h>
9 -
10 -#include "WslCoreNetworkingSupport.h"
11 -#include "RegistryWatcher.h"
12 -
13 -namespace wsl::core::networking {
14 -struct DnsInfo
15 -{
16 - std::vector<std::string> Servers;
17 - std::vector<std::string> Domains;
18 -};
19 -
20 -enum class DnsSettingsFlags
21 -{
22 - None = 0x0,
23 - IncludeVpn = 0x1,
24 - IncludeIpv6Servers = 0x2,
25 - IncludeAllSuffixes = 0x4
26 -};
27 -DEFINE_ENUM_FLAG_OPERATORS(DnsSettingsFlags);
28 -
29 -inline bool operator==(const DnsInfo& lhs, const DnsInfo& rhs) noexcept
30 -{
31 - return lhs.Servers == rhs.Servers && lhs.Domains == rhs.Domains;
32 -}
33 -inline bool operator!=(const DnsInfo& lhs, const DnsInfo& rhs) noexcept
34 -{
35 - return !(lhs == rhs);
36 -}
37 -
38 -std::string GenerateResolvConf(_In_ const DnsInfo& Info);
39 -
40 -/// <summary>
41 -/// Builds an hns::DNS notification from DnsInfo settings.
42 -/// </summary>
43 -/// <param name="settings">The DNS settings to convert</param>
44 -/// <param name="options">The resolv.conf header options (defaults to LX_INIT_RESOLVCONF_FULL_HEADER)</param>
45 -/// <returns>The hns::DNS notification ready to send via GNS channel</returns>
46 -wsl::shared::hns::DNS BuildDnsNotification(const DnsInfo& settings, PCWSTR options = LX_INIT_RESOLVCONF_FULL_HEADER);
47 -
48 -std::vector<std::string> GetAllDnsSuffixes(const std::vector<IpAdapterAddress>& AdapterAddresses);
49 -
50 -DWORD GetBestInterface();
51 -
52 -class HostDnsInfo
53 -{
54 -public:
55 - static DnsInfo GetDnsSettings(_In_ DnsSettingsFlags Flags);
56 -
57 - static DnsInfo GetDnsTunnelingSettings(const std::wstring& dnsTunnelingNameserver);
58 -
59 -private:
60 - /// <summary>
61 - /// Internal function to retrieve interface DNS servers.
62 - /// </summary>
63 - static std::vector<std::string> GetInterfaceDnsServers(const std::vector<IpAdapterAddress>& AdapterAddresses, _In_ DnsSettingsFlags Flags);
64 -
65 - /// <summary>
66 - /// Internal function to retrieve all Windows DNS suffixes.
67 - /// </summary>
68 - static std::vector<std::string> GetInterfaceDnsSuffixes(const std::vector<IpAdapterAddress>& AdapterAddresses);
69 -
70 - /// <summary>
71 - /// Internal function to convert DNS server addresses into strings.
72 - /// </summary>
73 - static std::vector<std::string> GetDnsServerStrings(_In_ const PIP_ADAPTER_DNS_SERVER_ADDRESS& DnsServer, _In_ USHORT IpFamilyFilter, _In_ USHORT MaxValues);
74 -};
75 -
76 -using RegistryChangeCallback = std::function<void()>;
77 -
78 -/// <summary>
79 -/// Class used to get notifications when Windows DNS suffixes are updated in registry.
80 -/// </summary>
81 -class DnsSuffixRegistryWatcher
82 -{
83 -public:
84 - DnsSuffixRegistryWatcher(RegistryChangeCallback&& reportRegistryChange);
85 - ~DnsSuffixRegistryWatcher() noexcept = default;
86 -
87 -private:
88 - RegistryChangeCallback m_reportRegistryChange;
89 -
90 - std::vector<wistd::unique_ptr<wsl::windows::common::slim_registry_watcher>> m_registryWatchers;
91 -};
92 -
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#pragma once
4 +#include <string>
5 +#include <vector>
6 +
7 +#include <iptypes.h>
8 +#include <wil/registry.h>
9 +
10 +#include "WslCoreNetworkingSupport.h"
11 +#include "RegistryWatcher.h"
12 +
13 +namespace wsl::core::networking {
14 +struct DnsInfo
15 +{
16 + std::vector<std::string> Servers;
17 + std::vector<std::string> Domains;
18 +};
19 +
20 +enum class DnsSettingsFlags
21 +{
22 + None = 0x0,
23 + IncludeVpn = 0x1,
24 + IncludeIpv6Servers = 0x2,
25 + IncludeAllSuffixes = 0x4
26 +};
27 +DEFINE_ENUM_FLAG_OPERATORS(DnsSettingsFlags);
28 +
29 +inline bool operator==(const DnsInfo& lhs, const DnsInfo& rhs) noexcept
30 +{
31 + return lhs.Servers == rhs.Servers && lhs.Domains == rhs.Domains;
32 +}
33 +inline bool operator!=(const DnsInfo& lhs, const DnsInfo& rhs) noexcept
34 +{
35 + return !(lhs == rhs);
36 +}
37 +
38 +std::string GenerateResolvConf(_In_ const DnsInfo& Info);
39 +
40 +/// <summary>
41 +/// Builds an hns::DNS notification from DnsInfo settings.
42 +/// </summary>
43 +/// <param name="settings">The DNS settings to convert</param>
44 +/// <param name="options">The resolv.conf header options (defaults to LX_INIT_RESOLVCONF_FULL_HEADER)</param>
45 +/// <returns>The hns::DNS notification ready to send via GNS channel</returns>
46 +wsl::shared::hns::DNS BuildDnsNotification(const DnsInfo& settings, PCWSTR options = LX_INIT_RESOLVCONF_FULL_HEADER);
47 +
48 +std::vector<std::string> GetAllDnsSuffixes(const std::vector<IpAdapterAddress>& AdapterAddresses);
49 +
50 +DWORD GetBestInterface();
51 +
52 +class HostDnsInfo
53 +{
54 +public:
55 + static DnsInfo GetDnsSettings(_In_ DnsSettingsFlags Flags);
56 +
57 + static DnsInfo GetDnsTunnelingSettings(const std::wstring& dnsTunnelingNameserver);
58 +
59 +private:
60 + /// <summary>
61 + /// Internal function to retrieve interface DNS servers.
62 + /// </summary>
63 + static std::vector<std::string> GetInterfaceDnsServers(const std::vector<IpAdapterAddress>& AdapterAddresses, _In_ DnsSettingsFlags Flags);
64 +
65 + /// <summary>
66 + /// Internal function to retrieve all Windows DNS suffixes.
67 + /// </summary>
68 + static std::vector<std::string> GetInterfaceDnsSuffixes(const std::vector<IpAdapterAddress>& AdapterAddresses);
69 +
70 + /// <summary>
71 + /// Internal function to convert DNS server addresses into strings.
72 + /// </summary>
73 + static std::vector<std::string> GetDnsServerStrings(_In_ const PIP_ADAPTER_DNS_SERVER_ADDRESS& DnsServer, _In_ USHORT IpFamilyFilter, _In_ USHORT MaxValues);
74 +};
75 +
76 +using RegistryChangeCallback = std::function<void()>;
77 +
78 +/// <summary>
79 +/// Class used to get notifications when Windows DNS suffixes are updated in registry.
80 +/// </summary>
81 +class DnsSuffixRegistryWatcher
82 +{
83 +public:
84 + DnsSuffixRegistryWatcher(RegistryChangeCallback&& reportRegistryChange);
85 + ~DnsSuffixRegistryWatcher() noexcept = default;
86 +
87 +private:
88 + RegistryChangeCallback m_reportRegistryChange;
89 +
90 + std::vector<wistd::unique_ptr<wsl::windows::common::slim_registry_watcher>> m_registryWatchers;
91 +};
92 +
93 } // namespace wsl::core::networking
\ No newline at end of file
src/windows/common/WslCoreMessageQueue.h
+359 -359
@@ -1,359 +1,359 @@
1 -/*++
2 -
3 -Copyright (c) Microsoft. All rights reserved.
4 -
5 -Module Name:
6 -
7 - WslCoreMessageQueue.h
8 -
9 -Abstract:
10 -
11 - This file contains a queuing implementation, guaranteeing running function objects
12 - with guaranteed serialization in a threadpool thread
13 -
14 ---*/
15 -
16 -#pragma once
17 -#include <deque>
18 -#include <functional>
19 -#include <memory>
20 -#include <variant>
21 -#include <windows.h>
22 -#include <wil/resource.h>
23 -
24 -namespace wsl::core {
25 -// forward-declare classes that can instantiate a WslThreadPoolWaitableResult object
26 -class WslCoreMessageQueue;
27 -
28 -class WslBaseThreadPoolWaitableResult
29 -{
30 -public:
31 - virtual ~WslBaseThreadPoolWaitableResult() noexcept = default;
32 -
33 -private:
34 - // limit who can run() and abort()
35 - friend class WslCoreMessageQueue;
36 -
37 - virtual void run() noexcept = 0;
38 - virtual void abort() noexcept = 0;
39 -};
40 -
41 -template <typename TReturn>
42 -class WslThreadPoolWaitableResult : public WslBaseThreadPoolWaitableResult
43 -{
44 -public:
45 - // throws a wil exception on failure
46 - template <typename FunctorType>
47 - explicit WslThreadPoolWaitableResult(FunctorType&& functor) : m_function(std::forward<FunctorType>(functor))
48 - {
49 - }
50 -
51 - ~WslThreadPoolWaitableResult() noexcept override = default;
52 -
53 - // returns ERROR_SUCCESS if the callback ran to completion
54 - // returns ERROR_TIMEOUT if this wait timed out
55 - // - this can be called multiple times if needing to probe
56 - // any other error code resulted from attempting to run the callback
57 - // - meaning it did *not* run to completion
58 - DWORD wait(DWORD timeout) const noexcept
59 - {
60 - if (!m_completionSignal.wait(timeout))
61 - {
62 - // not setting m_internalError to timeout
63 - // since the caller is allowed to try to wait() again later
64 - return ERROR_TIMEOUT;
65 - }
66 - const auto lock = m_lock.lock_shared();
67 - return m_internalError;
68 - }
69 -
70 - // waitable event handle, signaled when the callback has run to completion (or failed)
71 - HANDLE notification_event() const noexcept
72 - {
73 - return m_completionSignal.get();
74 - }
75 -
76 - const TReturn& read_result() const noexcept
77 - {
78 - return result;
79 - }
80 -
81 - // move the result out of the object for move-only types
82 - TReturn move_result() noexcept
83 - {
84 - TReturn move_out(std::move(result));
85 - return move_out;
86 - }
87 -
88 - // non-copyable
89 - WslThreadPoolWaitableResult(const WslThreadPoolWaitableResult&) = delete;
90 - WslThreadPoolWaitableResult& operator=(const WslThreadPoolWaitableResult&) = delete;
91 -
92 -private:
93 - void run() noexcept override
94 - {
95 - // we are now running in the TP callback
96 - {
97 - const auto lock = m_lock.lock_exclusive();
98 - if (m_runStatus != RunStatus::NotYetRun)
99 - {
100 - // return early - the caller has already canceled this
101 - return;
102 - }
103 - m_runStatus = RunStatus::Running;
104 - }
105 -
106 - DWORD error = NO_ERROR;
107 - try
108 - {
109 - result = std::move(m_function());
110 - }
111 - catch (...)
112 - {
113 - const HRESULT hr = wil::ResultFromCaughtException();
114 - // HRESULT_TO_WIN32
115 - error = (HRESULT_FACILITY(hr) == FACILITY_WIN32) ? HRESULT_CODE(hr) : hr;
116 - }
117 -
118 - const auto lock = m_lock.lock_exclusive();
119 - WI_ASSERT(m_runStatus == RunStatus::Running);
120 - m_runStatus = RunStatus::RanToCompletion;
121 - m_internalError = error;
122 - m_completionSignal.SetEvent();
123 - }
124 -
125 - void abort() noexcept override
126 - {
127 - const auto lock = m_lock.lock_exclusive();
128 - // only override the error if we know we haven't started running their functor
129 - if (m_runStatus == RunStatus::NotYetRun)
130 - {
131 - m_runStatus = RunStatus::Canceled;
132 - m_internalError = ERROR_CANCELLED;
133 - m_completionSignal.SetEvent();
134 - }
135 - }
136 -
137 - std::function<TReturn(void)> m_function;
138 - // a notification event
139 - wil::unique_event m_completionSignal{wil::EventOptions::ManualReset};
140 - mutable wil::srwlock m_lock;
141 - TReturn result{};
142 - DWORD m_internalError = NO_ERROR;
143 -
144 - enum class RunStatus
145 - {
146 - NotYetRun,
147 - Running,
148 - RanToCompletion,
149 - Canceled
150 - } m_runStatus{RunStatus::NotYetRun};
151 -};
152 -
153 -class WslCoreMessageQueue
154 -{
155 -public:
156 - WslCoreMessageQueue() : m_tpEnvironment(0, 1)
157 - {
158 - // create a single-threaded threadpool
159 - m_tpHandle = m_tpEnvironment.create_tp(WorkCallback, this);
160 - }
161 -
162 - template <typename TReturn, typename FunctorType>
163 - std::shared_ptr<WslThreadPoolWaitableResult<TReturn>> submit_with_results(FunctorType&& functor) noexcept
164 - try
165 - {
166 - FAIL_FAST_IF(m_tpHandle.get() == nullptr);
167 -
168 - const auto new_result = std::make_shared<WslThreadPoolWaitableResult<TReturn>>(std::forward<FunctorType>(functor));
169 - // scope to the queue lock
170 - {
171 - const auto queueLock = m_lock.lock_exclusive();
172 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANCELLED), m_isCanceled);
173 - m_workItems.emplace_back(new_result);
174 - }
175 -
176 - // always maintain a 1:1 ratio for calls to SubmitWorkWithResults() and ::SubmitThreadpoolWork
177 - SubmitThreadpoolWork(m_tpHandle.get());
178 - return new_result;
179 - }
180 - catch (...)
181 - {
182 - LOG_CAUGHT_EXCEPTION();
183 - return nullptr;
184 - }
185 -
186 - template <typename FunctorType>
187 - bool submit(FunctorType&& functor) noexcept
188 - try
189 - {
190 - FAIL_FAST_IF(m_tpHandle.get() == nullptr);
191 -
192 - // scope to the queue lock
193 - {
194 - const auto queueLock = m_lock.lock_exclusive();
195 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANCELLED), m_isCanceled);
196 - m_workItems.emplace_back(std::forward<SimpleFunction_t>(functor));
197 - }
198 -
199 - // always maintain a 1:1 ratio for calls to SubmitWork() and ::SubmitThreadpoolWork
200 - SubmitThreadpoolWork(m_tpHandle.get());
201 - return true;
202 - }
203 - catch (...)
204 - {
205 - LOG_CAUGHT_EXCEPTION();
206 - return false;
207 - }
208 -
209 - // functors must return type HRESULT
210 - template <typename FunctorType>
211 - HRESULT submit_and_wait(FunctorType&& functor) noexcept
212 - try
213 - {
214 - HRESULT hr = HRESULT_FROM_WIN32(ERROR_OUTOFMEMORY);
215 - if (const auto waitableResult = submit_with_results<HRESULT>(std::forward<FunctorType>(functor)))
216 - {
217 - hr = HRESULT_FROM_WIN32(waitableResult->wait(INFINITE));
218 - if (SUCCEEDED(hr))
219 - {
220 - hr = waitableResult->read_result();
221 - }
222 - }
223 - return hr;
224 - }
225 - CATCH_RETURN()
226 -
227 - // cancels anything queued to the TP - this WslCoreMessageQueue instance can no longer be used
228 - void cancel() noexcept
229 - try
230 - {
231 - if (m_tpHandle)
232 - {
233 - // immediately release anyone waiting for these workitems not yet run
234 - {
235 - const auto queueLock = m_lock.lock_exclusive();
236 - m_isCanceled = true;
237 -
238 - for (const auto& work : m_workItems)
239 - {
240 - // signal that these are canceled before we shutdown the TP which they could be scheduled
241 - if (const auto* pWaitableWorkitem = std::get_if<WaitableFunction_t>(&work))
242 - {
243 - (*pWaitableWorkitem)->abort();
244 - }
245 - }
246 -
247 - m_workItems.clear();
248 - }
249 -
250 - // force the m_tpHandle to wait and close the TP
251 - m_tpHandle.reset();
252 - m_tpEnvironment.reset();
253 - }
254 - }
255 - CATCH_LOG()
256 -
257 - bool isRunningInQueue() const noexcept
258 - {
259 - const auto currentThreadId = GetThreadId(GetCurrentThread());
260 - return currentThreadId == static_cast<DWORD>(InterlockedCompareExchange64(&m_threadpoolThreadId, 0ll, 0ll));
261 - }
262 -
263 - ~WslCoreMessageQueue() noexcept
264 - {
265 - cancel();
266 - }
267 -
268 - WslCoreMessageQueue(const WslCoreMessageQueue&) = delete;
269 - WslCoreMessageQueue& operator=(const WslCoreMessageQueue&) = delete;
270 - WslCoreMessageQueue(WslCoreMessageQueue&&) = delete;
271 - WslCoreMessageQueue& operator=(WslCoreMessageQueue&&) = delete;
272 -
273 -private:
274 - struct TPEnvironment
275 - {
276 - using unique_tp_env = wil::unique_struct<TP_CALLBACK_ENVIRON, decltype(&DestroyThreadpoolEnvironment), DestroyThreadpoolEnvironment>;
277 - unique_tp_env m_tpEnvironment;
278 -
279 - using unique_tp_pool = wil::unique_any<PTP_POOL, decltype(&CloseThreadpool), CloseThreadpool>;
280 - unique_tp_pool m_threadPool;
281 -
282 - TPEnvironment(DWORD countMinThread, DWORD countMaxThread)
283 - {
284 - InitializeThreadpoolEnvironment(&m_tpEnvironment);
285 -
286 - m_threadPool.reset(CreateThreadpool(nullptr));
287 - THROW_LAST_ERROR_IF_NULL(m_threadPool.get());
288 -
289 - // Set min and max thread counts for custom thread pool
290 - THROW_LAST_ERROR_IF(!::SetThreadpoolThreadMinimum(m_threadPool.get(), countMinThread));
291 - SetThreadpoolThreadMaximum(m_threadPool.get(), countMaxThread);
292 - SetThreadpoolCallbackPool(&m_tpEnvironment, m_threadPool.get());
293 - }
294 -
295 - wil::unique_threadpool_work create_tp(PTP_WORK_CALLBACK callback, void* pv)
296 - {
297 - wil::unique_threadpool_work newThreadpool(CreateThreadpoolWork(callback, pv, (m_threadPool) ? &m_tpEnvironment : nullptr));
298 - THROW_LAST_ERROR_IF_NULL(newThreadpool.get());
299 - return newThreadpool;
300 - }
301 -
302 - void reset()
303 - {
304 - m_threadPool.reset();
305 - m_tpEnvironment.reset();
306 - }
307 - };
308 -
309 - using SimpleFunction_t = std::function<void()>;
310 - using WaitableFunction_t = std::shared_ptr<WslBaseThreadPoolWaitableResult>;
311 - using FunctionVariant_t = std::variant<SimpleFunction_t, WaitableFunction_t>;
312 -
313 - // the lock must be destroyed *after* the TP object (thus must be declared first)
314 - // since the lock is used in the TP callback
315 - // the lock is mutable to allow us to acquire the lock in const methods
316 - mutable wil::srwlock m_lock;
317 - TPEnvironment m_tpEnvironment;
318 - wil::unique_threadpool_work m_tpHandle;
319 - std::deque<FunctionVariant_t> m_workItems;
320 - mutable LONG64 m_threadpoolThreadId{0}; // useful for callers to assert they are running within the queue
321 - bool m_isCanceled{false};
322 -
323 - static void CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE, void* Context, PTP_WORK) noexcept
324 - try
325 - {
326 - auto* pThis = static_cast<WslCoreMessageQueue*>(Context);
327 -
328 - FunctionVariant_t work;
329 - {
330 - const auto queueLock = pThis->m_lock.lock_exclusive();
331 -
332 - if (pThis->m_workItems.empty())
333 - {
334 - // pThis object is being destroyed and the queue was cleared
335 - return;
336 - }
337 -
338 - std::swap(work, pThis->m_workItems.front());
339 - pThis->m_workItems.pop_front();
340 -
341 - InterlockedExchange64(&pThis->m_threadpoolThreadId, GetThreadId(GetCurrentThread()));
342 - }
343 -
344 - // run the tasks outside the WslCoreMessageQueue lock
345 - const auto resetThreadIdOnExit = wil::scope_exit([pThis] { InterlockedExchange64(&pThis->m_threadpoolThreadId, 0ll); });
346 - if (work.index() == 0)
347 - {
348 - const auto& workItem = std::get<SimpleFunction_t>(work);
349 - workItem();
350 - }
351 - else
352 - {
353 - const auto& waitableWorkItem = std::get<WaitableFunction_t>(work);
354 - waitableWorkItem->run();
355 - }
356 - }
357 - CATCH_LOG()
358 -};
359 -} // namespace wsl::core
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WslCoreMessageQueue.h
8 +
9 +Abstract:
10 +
11 + This file contains a queuing implementation, guaranteeing running function objects
12 + with guaranteed serialization in a threadpool thread
13 +
14 +--*/
15 +
16 +#pragma once
17 +#include <deque>
18 +#include <functional>
19 +#include <memory>
20 +#include <variant>
21 +#include <windows.h>
22 +#include <wil/resource.h>
23 +
24 +namespace wsl::core {
25 +// forward-declare classes that can instantiate a WslThreadPoolWaitableResult object
26 +class WslCoreMessageQueue;
27 +
28 +class WslBaseThreadPoolWaitableResult
29 +{
30 +public:
31 + virtual ~WslBaseThreadPoolWaitableResult() noexcept = default;
32 +
33 +private:
34 + // limit who can run() and abort()
35 + friend class WslCoreMessageQueue;
36 +
37 + virtual void run() noexcept = 0;
38 + virtual void abort() noexcept = 0;
39 +};
40 +
41 +template <typename TReturn>
42 +class WslThreadPoolWaitableResult : public WslBaseThreadPoolWaitableResult
43 +{
44 +public:
45 + // throws a wil exception on failure
46 + template <typename FunctorType>
47 + explicit WslThreadPoolWaitableResult(FunctorType&& functor) : m_function(std::forward<FunctorType>(functor))
48 + {
49 + }
50 +
51 + ~WslThreadPoolWaitableResult() noexcept override = default;
52 +
53 + // returns ERROR_SUCCESS if the callback ran to completion
54 + // returns ERROR_TIMEOUT if this wait timed out
55 + // - this can be called multiple times if needing to probe
56 + // any other error code resulted from attempting to run the callback
57 + // - meaning it did *not* run to completion
58 + DWORD wait(DWORD timeout) const noexcept
59 + {
60 + if (!m_completionSignal.wait(timeout))
61 + {
62 + // not setting m_internalError to timeout
63 + // since the caller is allowed to try to wait() again later
64 + return ERROR_TIMEOUT;
65 + }
66 + const auto lock = m_lock.lock_shared();
67 + return m_internalError;
68 + }
69 +
70 + // waitable event handle, signaled when the callback has run to completion (or failed)
71 + HANDLE notification_event() const noexcept
72 + {
73 + return m_completionSignal.get();
74 + }
75 +
76 + const TReturn& read_result() const noexcept
77 + {
78 + return result;
79 + }
80 +
81 + // move the result out of the object for move-only types
82 + TReturn move_result() noexcept
83 + {
84 + TReturn move_out(std::move(result));
85 + return move_out;
86 + }
87 +
88 + // non-copyable
89 + WslThreadPoolWaitableResult(const WslThreadPoolWaitableResult&) = delete;
90 + WslThreadPoolWaitableResult& operator=(const WslThreadPoolWaitableResult&) = delete;
91 +
92 +private:
93 + void run() noexcept override
94 + {
95 + // we are now running in the TP callback
96 + {
97 + const auto lock = m_lock.lock_exclusive();
98 + if (m_runStatus != RunStatus::NotYetRun)
99 + {
100 + // return early - the caller has already canceled this
101 + return;
102 + }
103 + m_runStatus = RunStatus::Running;
104 + }
105 +
106 + DWORD error = NO_ERROR;
107 + try
108 + {
109 + result = std::move(m_function());
110 + }
111 + catch (...)
112 + {
113 + const HRESULT hr = wil::ResultFromCaughtException();
114 + // HRESULT_TO_WIN32
115 + error = (HRESULT_FACILITY(hr) == FACILITY_WIN32) ? HRESULT_CODE(hr) : hr;
116 + }
117 +
118 + const auto lock = m_lock.lock_exclusive();
119 + WI_ASSERT(m_runStatus == RunStatus::Running);
120 + m_runStatus = RunStatus::RanToCompletion;
121 + m_internalError = error;
122 + m_completionSignal.SetEvent();
123 + }
124 +
125 + void abort() noexcept override
126 + {
127 + const auto lock = m_lock.lock_exclusive();
128 + // only override the error if we know we haven't started running their functor
129 + if (m_runStatus == RunStatus::NotYetRun)
130 + {
131 + m_runStatus = RunStatus::Canceled;
132 + m_internalError = ERROR_CANCELLED;
133 + m_completionSignal.SetEvent();
134 + }
135 + }
136 +
137 + std::function<TReturn(void)> m_function;
138 + // a notification event
139 + wil::unique_event m_completionSignal{wil::EventOptions::ManualReset};
140 + mutable wil::srwlock m_lock;
141 + TReturn result{};
142 + DWORD m_internalError = NO_ERROR;
143 +
144 + enum class RunStatus
145 + {
146 + NotYetRun,
147 + Running,
148 + RanToCompletion,
149 + Canceled
150 + } m_runStatus{RunStatus::NotYetRun};
151 +};
152 +
153 +class WslCoreMessageQueue
154 +{
155 +public:
156 + WslCoreMessageQueue() : m_tpEnvironment(0, 1)
157 + {
158 + // create a single-threaded threadpool
159 + m_tpHandle = m_tpEnvironment.create_tp(WorkCallback, this);
160 + }
161 +
162 + template <typename TReturn, typename FunctorType>
163 + std::shared_ptr<WslThreadPoolWaitableResult<TReturn>> submit_with_results(FunctorType&& functor) noexcept
164 + try
165 + {
166 + FAIL_FAST_IF(m_tpHandle.get() == nullptr);
167 +
168 + const auto new_result = std::make_shared<WslThreadPoolWaitableResult<TReturn>>(std::forward<FunctorType>(functor));
169 + // scope to the queue lock
170 + {
171 + const auto queueLock = m_lock.lock_exclusive();
172 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANCELLED), m_isCanceled);
173 + m_workItems.emplace_back(new_result);
174 + }
175 +
176 + // always maintain a 1:1 ratio for calls to SubmitWorkWithResults() and ::SubmitThreadpoolWork
177 + SubmitThreadpoolWork(m_tpHandle.get());
178 + return new_result;
179 + }
180 + catch (...)
181 + {
182 + LOG_CAUGHT_EXCEPTION();
183 + return nullptr;
184 + }
185 +
186 + template <typename FunctorType>
187 + bool submit(FunctorType&& functor) noexcept
188 + try
189 + {
190 + FAIL_FAST_IF(m_tpHandle.get() == nullptr);
191 +
192 + // scope to the queue lock
193 + {
194 + const auto queueLock = m_lock.lock_exclusive();
195 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANCELLED), m_isCanceled);
196 + m_workItems.emplace_back(std::forward<SimpleFunction_t>(functor));
197 + }
198 +
199 + // always maintain a 1:1 ratio for calls to SubmitWork() and ::SubmitThreadpoolWork
200 + SubmitThreadpoolWork(m_tpHandle.get());
201 + return true;
202 + }
203 + catch (...)
204 + {
205 + LOG_CAUGHT_EXCEPTION();
206 + return false;
207 + }
208 +
209 + // functors must return type HRESULT
210 + template <typename FunctorType>
211 + HRESULT submit_and_wait(FunctorType&& functor) noexcept
212 + try
213 + {
214 + HRESULT hr = HRESULT_FROM_WIN32(ERROR_OUTOFMEMORY);
215 + if (const auto waitableResult = submit_with_results<HRESULT>(std::forward<FunctorType>(functor)))
216 + {
217 + hr = HRESULT_FROM_WIN32(waitableResult->wait(INFINITE));
218 + if (SUCCEEDED(hr))
219 + {
220 + hr = waitableResult->read_result();
221 + }
222 + }
223 + return hr;
224 + }
225 + CATCH_RETURN()
226 +
227 + // cancels anything queued to the TP - this WslCoreMessageQueue instance can no longer be used
228 + void cancel() noexcept
229 + try
230 + {
231 + if (m_tpHandle)
232 + {
233 + // immediately release anyone waiting for these workitems not yet run
234 + {
235 + const auto queueLock = m_lock.lock_exclusive();
236 + m_isCanceled = true;
237 +
238 + for (const auto& work : m_workItems)
239 + {
240 + // signal that these are canceled before we shutdown the TP which they could be scheduled
241 + if (const auto* pWaitableWorkitem = std::get_if<WaitableFunction_t>(&work))
242 + {
243 + (*pWaitableWorkitem)->abort();
244 + }
245 + }
246 +
247 + m_workItems.clear();
248 + }
249 +
250 + // force the m_tpHandle to wait and close the TP
251 + m_tpHandle.reset();
252 + m_tpEnvironment.reset();
253 + }
254 + }
255 + CATCH_LOG()
256 +
257 + bool isRunningInQueue() const noexcept
258 + {
259 + const auto currentThreadId = GetThreadId(GetCurrentThread());
260 + return currentThreadId == static_cast<DWORD>(InterlockedCompareExchange64(&m_threadpoolThreadId, 0ll, 0ll));
261 + }
262 +
263 + ~WslCoreMessageQueue() noexcept
264 + {
265 + cancel();
266 + }
267 +
268 + WslCoreMessageQueue(const WslCoreMessageQueue&) = delete;
269 + WslCoreMessageQueue& operator=(const WslCoreMessageQueue&) = delete;
270 + WslCoreMessageQueue(WslCoreMessageQueue&&) = delete;
271 + WslCoreMessageQueue& operator=(WslCoreMessageQueue&&) = delete;
272 +
273 +private:
274 + struct TPEnvironment
275 + {
276 + using unique_tp_env = wil::unique_struct<TP_CALLBACK_ENVIRON, decltype(&DestroyThreadpoolEnvironment), DestroyThreadpoolEnvironment>;
277 + unique_tp_env m_tpEnvironment;
278 +
279 + using unique_tp_pool = wil::unique_any<PTP_POOL, decltype(&CloseThreadpool), CloseThreadpool>;
280 + unique_tp_pool m_threadPool;
281 +
282 + TPEnvironment(DWORD countMinThread, DWORD countMaxThread)
283 + {
284 + InitializeThreadpoolEnvironment(&m_tpEnvironment);
285 +
286 + m_threadPool.reset(CreateThreadpool(nullptr));
287 + THROW_LAST_ERROR_IF_NULL(m_threadPool.get());
288 +
289 + // Set min and max thread counts for custom thread pool
290 + THROW_LAST_ERROR_IF(!::SetThreadpoolThreadMinimum(m_threadPool.get(), countMinThread));
291 + SetThreadpoolThreadMaximum(m_threadPool.get(), countMaxThread);
292 + SetThreadpoolCallbackPool(&m_tpEnvironment, m_threadPool.get());
293 + }
294 +
295 + wil::unique_threadpool_work create_tp(PTP_WORK_CALLBACK callback, void* pv)
296 + {
297 + wil::unique_threadpool_work newThreadpool(CreateThreadpoolWork(callback, pv, (m_threadPool) ? &m_tpEnvironment : nullptr));
298 + THROW_LAST_ERROR_IF_NULL(newThreadpool.get());
299 + return newThreadpool;
300 + }
301 +
302 + void reset()
303 + {
304 + m_threadPool.reset();
305 + m_tpEnvironment.reset();
306 + }
307 + };
308 +
309 + using SimpleFunction_t = std::function<void()>;
310 + using WaitableFunction_t = std::shared_ptr<WslBaseThreadPoolWaitableResult>;
311 + using FunctionVariant_t = std::variant<SimpleFunction_t, WaitableFunction_t>;
312 +
313 + // the lock must be destroyed *after* the TP object (thus must be declared first)
314 + // since the lock is used in the TP callback
315 + // the lock is mutable to allow us to acquire the lock in const methods
316 + mutable wil::srwlock m_lock;
317 + TPEnvironment m_tpEnvironment;
318 + wil::unique_threadpool_work m_tpHandle;
319 + std::deque<FunctionVariant_t> m_workItems;
320 + mutable LONG64 m_threadpoolThreadId{0}; // useful for callers to assert they are running within the queue
321 + bool m_isCanceled{false};
322 +
323 + static void CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE, void* Context, PTP_WORK) noexcept
324 + try
325 + {
326 + auto* pThis = static_cast<WslCoreMessageQueue*>(Context);
327 +
328 + FunctionVariant_t work;
329 + {
330 + const auto queueLock = pThis->m_lock.lock_exclusive();
331 +
332 + if (pThis->m_workItems.empty())
333 + {
334 + // pThis object is being destroyed and the queue was cleared
335 + return;
336 + }
337 +
338 + std::swap(work, pThis->m_workItems.front());
339 + pThis->m_workItems.pop_front();
340 +
341 + InterlockedExchange64(&pThis->m_threadpoolThreadId, GetThreadId(GetCurrentThread()));
342 + }
343 +
344 + // run the tasks outside the WslCoreMessageQueue lock
345 + const auto resetThreadIdOnExit = wil::scope_exit([pThis] { InterlockedExchange64(&pThis->m_threadpoolThreadId, 0ll); });
346 + if (work.index() == 0)
347 + {
348 + const auto& workItem = std::get<SimpleFunction_t>(work);
349 + workItem();
350 + }
351 + else
352 + {
353 + const auto& waitableWorkItem = std::get<WaitableFunction_t>(work);
354 + waitableWorkItem->run();
355 + }
356 + }
357 + CATCH_LOG()
358 +};
359 +} // namespace wsl::core
src/windows/common/WslCoreNetworkEndpointSettings.cpp
+6 -1
@@ -152,7 +152,12 @@ std::wstring wsl::core::networking::NetworkSettings::GetBestGatewayMacAddress(AD
152 const auto result = ResolveIpNetEntry2(&ipNetRow, nullptr);
153 if (result != NO_ERROR)
154 {
155 - LOG_HR_MSG(HRESULT_FROM_WIN32(result), "Failed to resolve gateway MAC address");
155 + LOG_HR_MSG(
156 + HRESULT_FROM_WIN32(result),
157 + "Failed to resolve gateway MAC address for: %ls, interface: %lu",
158 + windows::common::string::SockAddrInetToWstring(gatewayAddress).c_str(),
159 + InterfaceIndex);
160 +
161 return {};
162 }
163
src/windows/common/WslCoreNetworkingSupport.h
+20 -6
@@ -148,17 +148,31 @@ using unique_address_table = wil::unique_any<PMIB_UNICASTIPADDRESS_TABLE, declty
148 using unique_forward_table = wil::unique_any<PMIB_IPFORWARD_TABLE2, decltype(FreeMibTable), &FreeMibTable>;
149 using unique_ifstack_table = wil::unique_any<PMIB_IFSTACK_TABLE, decltype(FreeMibTable), &FreeMibTable>;
150
151 +// Ensures COM is initialized on the current thread. Tolerates the case where
152 +// COM is already initialized (even with a different apartment type or security) since this
153 +// can happen on RPC threads or callback threads.
154 inline wil::unique_couninitialize_call InitializeCOMState()
155 {
153 - // Ensure COM is initialized
154 - auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
155 - HRESULT hr = CoInitializeSecurity(
156 - nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_STATIC_CLOAKING, nullptr);
157 - // Ignore error if CoInitializeSecurity has already been invoked
158 - if (hr == RPC_E_TOO_LATE)
156 + wil::unique_couninitialize_call coInit;
157 + auto hr = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED);
158 + if (SUCCEEDED(hr))
159 {
160 + // Ignore error if CoInitializeSecurity has already been invoked
161 + hr = CoInitializeSecurity(
162 + nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_STATIC_CLOAKING, nullptr);
163 +
164 + if (hr == RPC_E_TOO_LATE)
165 + {
166 + hr = S_OK;
167 + }
168 + }
169 + else if (hr == RPC_E_CHANGED_MODE)
170 + {
171 + // COM already initialized by someone else - disarm so we don't uninitialize their COM
172 + coInit.release();
173 hr = S_OK;
174 }
175 +
176 THROW_IF_FAILED(hr);
177 return coInit;
178 }
src/windows/common/WslInstall.cpp
+18 -33
@@ -23,16 +23,12 @@ Abstract:
23 extern HINSTANCE g_dllInstance;
24
25 constexpr LPCWSTR c_optionalFeatureInstallStatus = L"InstallStatus";
26 -constexpr LPCWSTR c_optionalFeatureNameVmp = L"VirtualMachinePlatform";
27 -constexpr LPCWSTR c_optionalFeatureNameWsl = L"Microsoft-Windows-Subsystem-Linux";
26
27 using wsl::shared::Localization;
28 using namespace wsl::windows::common::distribution;
29 using namespace wsl::windows::common::wslutil;
30
31 namespace {
34 -std::vector<BYTE> ParseHex(const std::wstring& input);
35 -
32 void EnforceFileHash(HANDLE file, const std::wstring& expectedHash)
33 {
34 wsl::windows::common::ExecutionContext context(wsl::windows::common::VerifyChecksum);
@@ -40,7 +36,7 @@ void EnforceFileHash(HANDLE file, const std::wstring& expectedHash)
36 const auto fileHash = wsl::windows::common::wslutil::HashFile(file, CALG_SHA_256);
37
38 THROW_LAST_ERROR_IF(SetFilePointer(file, 0, 0, FILE_BEGIN) == INVALID_SET_FILE_POINTER);
43 - if (fileHash != ParseHex(expectedHash))
39 + if (fileHash != wsl::windows::common::string::HexToBytes(expectedHash))
40 {
41 THROW_HR_WITH_USER_ERROR(
42 TRUST_E_BAD_DIGEST,
@@ -64,31 +60,6 @@ std::vector<std::wstring> GetInstalledOptionalComponents()
60 return installedComponents;
61 }
62
67 -std::vector<BYTE> ParseHex(const std::wstring& input)
68 -{
69 - std::vector<BYTE> result;
70 - for (auto i = 0; i < input.size(); i += 2)
71 - {
72 - // Skip '0x', if any
73 - if (i == 0 && input.size() >= 2 && input[0] == '0' && tolower(input[1]) == 'x')
74 - {
75 - continue;
76 - }
77 -
78 - auto current = input.substr(i, 2);
79 - wchar_t* endPtr{};
80 -
81 - const auto byte = wcstoul(current.data(), &endPtr, 16);
82 - if (endPtr != current.data() + 2)
83 - {
84 - THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageInvalidHexString(input.c_str()));
85 - }
86 -
87 - result.push_back(static_cast<BYTE>(byte));
88 - }
89 -
90 - return result;
91 -}
63 }; // namespace
64
65 HRESULT WslInstall::InstallDistribution(
@@ -239,18 +210,32 @@ std::pair<bool, std::vector<std::wstring>> WslInstall::CheckForMissingOptionalCo
210 return {rebootRequired, std::move(missingComponents)};
211 }
212
242 -void WslInstall::InstallOptionalComponents(const std::vector<std::wstring>& components)
213 +DWORD WslInstall::InstallOptionalComponent(LPCWSTR component, bool consoleOutput)
214 {
215 std::wstring systemDirectory;
216 THROW_IF_FAILED(wil::GetSystemDirectoryW(systemDirectory));
217
218 const auto dismPath = std::filesystem::path(std::move(systemDirectory)) / L"dism.exe";
219 +
220 + auto commandLine = std::format(L"{} /Online /NoRestart /enable-feature /featurename:{}", dismPath.native(), component);
221 +
222 + wsl::windows::common::SubProcess process(nullptr, commandLine.c_str());
223 + if (!consoleOutput)
224 + {
225 + process.SetFlags(CREATE_NEW_CONSOLE);
226 + process.SetShowWindow(SW_HIDE);
227 + }
228 +
229 + return process.Run();
230 +}
231 +
232 +void WslInstall::InstallOptionalComponents(const std::vector<std::wstring>& components)
233 +{
234 for (const auto& component : components)
235 {
236 wsl::windows::common::wslutil::PrintMessage(Localization::MessageInstallingWindowsComponent(component));
237
252 - auto commandLine = std::format(L"{} /Online /NoRestart /enable-feature /featurename:{}", dismPath.wstring(), component);
253 - const auto exitCode = wsl::windows::common::helpers::RunProcess(commandLine);
238 + const auto exitCode = InstallOptionalComponent(component.c_str(), true);
239 if (exitCode != 0 && exitCode != ERROR_SUCCESS_REBOOT_REQUIRED)
240 {
241 THROW_HR_WITH_USER_ERROR(WSL_E_INSTALL_COMPONENT_FAILED, Localization::MessageOptionalComponentInstallFailed(component, exitCode));
src/windows/common/WslInstall.h
+5
@@ -19,6 +19,9 @@ Abstract:
19 class WslInstall
20 {
21 public:
22 + static inline LPCWSTR c_optionalFeatureNameVmp = L"VirtualMachinePlatform";
23 + static inline LPCWSTR c_optionalFeatureNameWsl = L"Microsoft-Windows-Subsystem-Linux";
24 +
25 struct InstallResult
26 {
27 std::wstring Name;
@@ -44,6 +47,8 @@ public:
47
48 static void InstallOptionalComponents(const std::vector<std::wstring>& components);
49
50 + static DWORD InstallOptionalComponent(LPCWSTR component, bool consoleOutput);
51 +
52 static std::pair<std::wstring, GUID> InstallModernDistribution(
53 const wsl::windows::common::distribution::ModernDistributionVersion& distribution,
54 const std::optional<ULONG>& version,
src/windows/common/WslSecurity.cpp
+15
@@ -99,6 +99,21 @@ wil::unique_handle wsl::windows::common::security::CreateRestrictedToken(_In_ HA
99 return restrictedToken;
100 }
101
102 +void wsl::windows::common::security::ConfigureForCOMImpersonation(IUnknown* Instance)
103 +{
104 + wil::com_ptr_nothrow<IClientSecurity> clientSecurity;
105 + THROW_IF_FAILED(Instance->QueryInterface(IID_PPV_ARGS(&clientSecurity)));
106 +
107 + // Get the current proxy blanket settings.
108 + DWORD authnSvc, authzSvc, authnLvl, capabilites;
109 + THROW_IF_FAILED(clientSecurity->QueryBlanket(Instance, &authnSvc, &authzSvc, NULL, &authnLvl, NULL, NULL, &capabilites));
110 +
111 + // Make sure that dynamic cloaking is used.
112 + WI_ClearFlag(capabilites, EOAC_STATIC_CLOAKING);
113 + WI_SetFlag(capabilites, EOAC_DYNAMIC_CLOAKING);
114 + THROW_IF_FAILED(clientSecurity->SetBlanket(Instance, authnSvc, authzSvc, NULL, authnLvl, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, capabilites));
115 +}
116 +
117 LUID wsl::windows::common::security::EnableTokenPrivilege(_Inout_ HANDLE token, _In_ LPCWSTR privilegeName)
118 {
119 // Convert privilege name to an LUID.
src/windows/common/WslSecurity.h
+5
@@ -87,6 +87,11 @@ std::pair<PSID, std::vector<char>> CreateSid(SID_IDENTIFIER_AUTHORITY Authority,
87 /// </summary>
88 wil::unique_handle CreateRestrictedToken(_In_ HANDLE token);
89
90 +/// <summary>
91 +/// Configures a COM object for impersonation.
92 +/// <summary>
93 +void ConfigureForCOMImpersonation(IUnknown* instance);
94 +
95 /// <summary>
96 /// Enables a privilege on the token.
97 /// </summary>
src/windows/common/WslTelemetry.cpp
+7
@@ -32,6 +32,13 @@ TRACELOGGING_DEFINE_PROVIDER(
32 (0xb99cdb5a, 0x039c, 0x5046, 0xe6, 0x72, 0x1a, 0x0d, 0xe0, 0xa4, 0x02, 0x11),
33 TraceLoggingOptionMicrosoftTelemetry());
34
35 +TRACELOGGING_DEFINE_PROVIDER(
36 + WslcTelemetryProvider,
37 + "Microsoft.Windows.Wslc",
38 + // {0383CE62-8F86-4766-AFB2-9D66A7FB1E90}
39 + (0x383ce62, 0x8f86, 0x4766, 0xaf, 0xb2, 0x9d, 0x66, 0xa7, 0xfb, 0x1e, 0x90),
40 + TraceLoggingOptionMicrosoftTelemetry());
41 +
42 #ifdef DEBUG
43 #define HRESULT_STRING_VALUE \
44 , TraceLoggingValue(wsl::windows::common::wslutil::ErrorCodeToString(failure->hr).c_str(), "HRESULTString")
src/windows/common/WslTelemetry.h
+1
@@ -28,6 +28,7 @@ extern "C" {
28 #endif
29 TRACELOGGING_DECLARE_PROVIDER(LxssTelemetryProvider);
30 TRACELOGGING_DECLARE_PROVIDER(WslServiceTelemetryProvider);
31 +TRACELOGGING_DECLARE_PROVIDER(WslcTelemetryProvider);
32 #ifdef __cplusplus
33 }
34 #endif
src/windows/common/hcs.cpp
+20 -1
@@ -42,6 +42,17 @@ void wsl::windows::common::hcs::AddPlan9Share(
42 ModifyComputeSystem(ComputeSystem, wsl::shared::ToJsonW(request).c_str(), UserToken);
43 }
44
45 +void wsl::windows::common::hcs::RemovePlan9Share(_In_ HCS_SYSTEM ComputeSystem, _In_ PCWSTR AccessName, _In_ UINT32 Port)
46 +{
47 + ModifySettingRequest<Plan9Share> request{};
48 + request.RequestType = ModifyRequestType::Remove;
49 + request.ResourcePath = L"VirtualMachine/Devices/Plan9/Shares";
50 + request.Settings.AccessName = AccessName;
51 + request.Settings.Port = Port;
52 +
53 + ModifyComputeSystem(ComputeSystem, wsl::shared::ToJsonW(request).c_str());
54 +}
55 +
56 void wsl::windows::common::hcs::AddVhd(_In_ HCS_SYSTEM ComputeSystem, _In_ PCWSTR VhdPath, _In_ ULONG Lun, _In_ bool ReadOnly)
57 {
58 ModifySettingRequest<Attachment> request{};
@@ -251,7 +262,7 @@ void wsl::windows::common::hcs::RevokeVmAccess(_In_ PCWSTR VmId, _In_ PCWSTR Fil
262 {
263 WSL_LOG_DEBUG("HcsRevokeVmAccess", TraceLoggingValue(VmId, "vmId"), TraceLoggingValue(FilePath, "filePath"));
264
254 - ExecutionContext context(Context::HNS);
265 + ExecutionContext context(Context::HCS);
266
267 THROW_IF_FAILED_MSG(::HcsRevokeVmAccess(VmId, FilePath), "HcsRevokeVmAccess(%ls, %ls)", VmId, FilePath);
268 }
@@ -313,4 +324,12 @@ wsl::windows::common::hcs::unique_hcn_guest_network_service_callback wsl::window
324 THROW_IF_FAILED(::HcnRegisterGuestNetworkServiceCallback(GuestNetworkService.get(), Callback, Context, &callbackHandle));
325
326 return callbackHandle;
327 +}
328 +
329 +bool wsl::windows::common::hcs::IsDisableVgpuSettingsSupported()
330 +{
331 + static constexpr std::pair<uint32_t, uint32_t> c_schemaVersionNickel{2, 7};
332 +
333 + // See if the Windows version has the required platform change.
334 + return ((GetSchemaVersion() >= c_schemaVersionNickel) && (wsl::windows::common::helpers::GetWindowsVersion().BuildNumber >= 22545));
335 }
\ No newline at end of file
src/windows/common/hcs.hpp
+4
@@ -45,6 +45,8 @@ void AddPlan9Share(
45 _In_ Plan9ShareFlags Flags,
46 _In_opt_ HANDLE UserToken = nullptr);
47
48 +void RemovePlan9Share(_In_ HCS_SYSTEM ComputeSystem, _In_ PCWSTR AccessName, _In_ UINT32 Port);
49 +
50 void AddVhd(_In_ HCS_SYSTEM ComputeSystem, _In_ PCWSTR VhdPath, _In_ ULONG Lun, _In_ bool ReadOnly = false);
51
52 void AddPassThroughDisk(_In_ HCS_SYSTEM ComputeSystem, _In_ PCWSTR Disk, _In_ ULONG Lun);
@@ -82,4 +84,6 @@ unique_hcn_service_callback RegisterServiceCallback(_In_ HCS_NOTIFICATION_CALLBA
84 unique_hcn_guest_network_service_callback RegisterGuestNetworkServiceCallback(
85 _In_ const unique_hcn_guest_network_service& GuestNetworkService, _In_ HCS_NOTIFICATION_CALLBACK Callback, _In_ PVOID Context);
86
87 +bool IsDisableVgpuSettingsSupported();
88 +
89 } // namespace wsl::windows::common::hcs
src/windows/common/hcs_schema.h
+175 -2
@@ -22,6 +22,12 @@ Abstract:
22 Json[#Value] = (Object).Value.value(); \
23 }
24
25 +#define ASSIGN_IF_PRESENT(Json, Object, Value) \
26 + if (Json.contains(#Value)) \
27 + { \
28 + (Object).Value = Json.at(#Value).get_to((Object).Value); \
29 + }
30 +
31 namespace wsl::windows::common::hcs {
32
33 enum class ModifyRequestType
@@ -439,6 +445,66 @@ struct Scsi
445 NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Scsi, Attachments);
446 };
447
448 +struct DebugOptions
449 +{
450 + std::optional<std::wstring> BugcheckSavedStateFileName;
451 + std::optional<std::wstring> ShutdownOrResetSavedStateFileName;
452 +};
453 +
454 +inline void to_json(nlohmann::json& j, const DebugOptions& d)
455 +{
456 + j = nlohmann::json::object();
457 + OMIT_IF_EMPTY(j, d, BugcheckSavedStateFileName);
458 + OMIT_IF_EMPTY(j, d, ShutdownOrResetSavedStateFileName);
459 +}
460 +
461 +enum class VirtualPMemImageFormat
462 +{
463 + Vhdx,
464 + Vhd1
465 +};
466 +
467 +NLOHMANN_JSON_SERIALIZE_ENUM(
468 + VirtualPMemImageFormat,
469 + {
470 + {VirtualPMemImageFormat::Vhdx, "Vhdx"},
471 + {VirtualPMemImageFormat::Vhd1, "Vhd1"},
472 + })
473 +
474 +struct VirtualPMemDevice
475 +{
476 + std::wstring HostPath;
477 + bool ReadOnly;
478 + VirtualPMemImageFormat ImageFormat;
479 + // uint64_t SizeBytes;
480 + // std::map<uint64_t, VirtualPMemMapping> Mappings;
481 +
482 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(VirtualPMemDevice, HostPath, ReadOnly, ImageFormat);
483 +};
484 +
485 +enum class VirtualPMemBackingType
486 +{
487 + Virtual,
488 + Physical
489 +};
490 +
491 +NLOHMANN_JSON_SERIALIZE_ENUM(
492 + VirtualPMemBackingType,
493 + {
494 + {VirtualPMemBackingType::Virtual, "Virtual"},
495 + {VirtualPMemBackingType::Physical, "Physical"},
496 + })
497 +
498 +struct VirtualPMemController
499 +{
500 + std::map<std::string, VirtualPMemDevice> Devices;
501 + uint8_t MaximumCount;
502 + uint64_t MaximumSizeBytes;
503 + VirtualPMemBackingType Backing;
504 +
505 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(VirtualPMemController, Devices, MaximumCount, MaximumSizeBytes, Backing);
506 +};
507 +
508 struct Devices
509 {
510 std::optional<VirtioSerial> VirtioSerial;
@@ -447,6 +513,7 @@ struct Devices
513 EmptyObject Battery;
514 HvSocket HvSocket;
515 std::map<std::string, Scsi> Scsi;
516 + std::optional<VirtualPMemController> VirtualPMem;
517 };
518
519 inline void to_json(nlohmann::json& j, const Devices& devices)
@@ -459,6 +526,7 @@ inline void to_json(nlohmann::json& j, const Devices& devices)
526 {"Scsi", devices.Scsi}};
527
528 OMIT_IF_EMPTY(j, devices, VirtioSerial);
529 + OMIT_IF_EMPTY(j, devices, VirtualPMem);
530 }
531
532 struct VirtualMachine
@@ -467,8 +535,9 @@ struct VirtualMachine
535 Chipset Chipset;
536 Topology ComputeTopology;
537 Devices Devices;
538 + DebugOptions DebugOptions;
539
471 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(VirtualMachine, StopOnReset, Chipset, ComputeTopology, Devices);
540 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(VirtualMachine, StopOnReset, Chipset, ComputeTopology, Devices, DebugOptions);
541 };
542
543 struct ComputeSystem
@@ -481,13 +550,117 @@ struct ComputeSystem
550 NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ComputeSystem, Owner, ShouldTerminateOnLastHandleClosed, SchemaVersion, VirtualMachine)
551 };
552
553 +struct GuestErrorSaveReport
554 +{
555 + std::optional<std::wstring> SaveStateFile;
556 + std::optional<long> Status;
557 +};
558 +
559 +inline void to_json(nlohmann::json& j, const GuestErrorSaveReport& g)
560 +{
561 + j = nlohmann::json::object();
562 + OMIT_IF_EMPTY(j, g, SaveStateFile);
563 + OMIT_IF_EMPTY(j, g, Status);
564 +}
565 +
566 +inline void from_json(const nlohmann::json& j, GuestErrorSaveReport& r)
567 +{
568 + ASSIGN_IF_PRESENT(j, r, SaveStateFile);
569 + ASSIGN_IF_PRESENT(j, r, Status);
570 +}
571 +
572 struct CrashReport
573 {
574 std::wstring CrashLog;
575 + std::optional<GuestErrorSaveReport> GuestCrashSaveInfo;
576 +};
577 +
578 +inline void to_json(nlohmann::json& j, const CrashReport& c)
579 +{
580 + j = nlohmann::json::object();
581 + j.at("CrashLog") = c.CrashLog;
582 + OMIT_IF_EMPTY(j, c, GuestCrashSaveInfo);
583 +}
584 +
585 +inline void from_json(const nlohmann::json& j, CrashReport& c)
586 +{
587 + ASSIGN_IF_PRESENT(j, c, CrashLog);
588 + ASSIGN_IF_PRESENT(j, c, GuestCrashSaveInfo);
589 +}
590 +
591 +enum class NotificationType
592 +{
593 + None,
594 + GracefulExit,
595 + ForcedExit,
596 + UnexpectedExit,
597 + Unknown
598 +};
599 +
600 +NLOHMANN_JSON_SERIALIZE_ENUM(
601 + NotificationType,
602 + {
603 + {NotificationType::None, "None"},
604 + {NotificationType::GracefulExit, "GracefulExit"},
605 + {NotificationType::ForcedExit, "ForcedExit"},
606 + {NotificationType::UnexpectedExit, "UnexpectedExit"},
607 + {NotificationType::Unknown, "Unknown"},
608 + })
609 +
610 +struct GuestCrashAttribution
611 +{
612 + std::optional<std::vector<uint64_t>> CrashParameters;
613 +};
614 +
615 +inline void to_json(nlohmann::json& j, const GuestCrashAttribution& g)
616 +{
617 + j = nlohmann::json::object();
618 + OMIT_IF_EMPTY(j, g, CrashParameters)
619 +}
620 +
621 +inline void from_json(const nlohmann::json& j, GuestCrashAttribution& g)
622 +{
623 + ASSIGN_IF_PRESENT(j, g, CrashParameters);
624 +}
625
488 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CrashReport, CrashLog);
626 +// Attribution record (trimmed to GuestCrash only for now)
627 +struct AttributionRecord
628 +{
629 + std::optional<GuestCrashAttribution> GuestCrash;
630 };
631
632 +inline void to_json(nlohmann::json& j, const AttributionRecord& a)
633 +{
634 + j = nlohmann::json::object();
635 + OMIT_IF_EMPTY(j, a, GuestCrash)
636 +}
637 +
638 +inline void from_json(const nlohmann::json& j, AttributionRecord& a)
639 +{
640 + ASSIGN_IF_PRESENT(j, a, GuestCrash);
641 +}
642 +
643 +struct SystemExitStatus
644 +{
645 + int32_t Status;
646 + std::optional<NotificationType> ExitType;
647 + std::optional<std::vector<AttributionRecord>> Attribution;
648 +};
649 +
650 +inline void to_json(nlohmann::json& j, const SystemExitStatus& s)
651 +{
652 + j = nlohmann::json{{"Status", s.Status}};
653 + OMIT_IF_EMPTY(j, s, ExitType);
654 + OMIT_IF_EMPTY(j, s, Attribution);
655 +}
656 +
657 +inline void from_json(const nlohmann::json& j, SystemExitStatus& s)
658 +{
659 + s.Status = j.at("Status").get<int32_t>();
660 + ASSIGN_IF_PRESENT(j, s, ExitType);
661 + ASSIGN_IF_PRESENT(j, s, Attribution);
662 +}
663 +
664 } // namespace wsl::windows::common::hcs
665
666 #undef OMIT_IF_EMPTY
\ No newline at end of file
src/windows/common/helpers.cpp
+72
@@ -26,6 +26,14 @@ Abstract:
26 #include "versionhelpers.h"
27 #include <regstr.h>
28
29 +// Version numbers for various functionality that was backported.
30 +
31 +#define VIRTIO_SERIAL_CONSOLE_COBALT_RELEASE_UBR 40
32 +#define NICKEL_BUILD_FLOOR 22350
33 +#define VMEMM_SUFFIX_COBALT_REFRESH_BUILD_NUMBER 22138
34 +#define VMMEM_SUFFIX_COBALT_RELEASE_UBR 71
35 +#define VMMEM_SUFFIX_NICKEL_BUILD_NUMBER 22420
36 +
37 using wsl::windows::common::helpers::LaunchWslRelayFlags;
38
39 constexpr auto c_WslSupportInterfaceKey = L"Software\\Classes\\Interface\\{46f3c96d-ffa3-42f0-b052-52f5e7ecbb08}";
@@ -467,6 +475,52 @@ bool wsl::windows::common::helpers::IsServicePresent(_In_ LPCWSTR ServiceName)
475 return !!service;
476 }
477
478 +bool wsl::windows::common::helpers::IsServiceRunning(_In_ LPCWSTR ServiceName)
479 +{
480 + const wil::unique_schandle manager{OpenSCManager(nullptr, nullptr, SC_MANAGER_CONNECT)};
481 + if (!manager)
482 + {
483 + return false;
484 + }
485 +
486 + const wil::unique_schandle service{OpenServiceW(manager.get(), ServiceName, SERVICE_QUERY_STATUS)};
487 + if (!service)
488 + {
489 + return false;
490 + }
491 +
492 + SERVICE_STATUS status;
493 + if (!QueryServiceStatus(service.get(), &status))
494 + {
495 + return false;
496 + }
497 +
498 + return status.dwCurrentState != SERVICE_STOPPED;
499 +}
500 +
501 +bool wsl::windows::common::helpers::IsVirtioSerialConsoleSupported()
502 +{
503 + // See if the Windows version has the required platform change.
504 + //
505 + // N.B. If the package is running on a vibranium or iron build, then it means that lifted
506 + // support is available, so virtio serial is available as well (since it was done in the same LCU).
507 +
508 + auto windowsVersion = GetWindowsVersion();
509 + return windowsVersion.BuildNumber != WindowsBuildNumbers::Cobalt ||
510 + windowsVersion.UpdateBuildRevision >= VIRTIO_SERIAL_CONSOLE_COBALT_RELEASE_UBR;
511 +}
512 +
513 +bool wsl::windows::common::helpers::IsVmemmSuffixSupported()
514 +{
515 + auto windowsVersion = GetWindowsVersion();
516 +
517 + // See if the Windows version has the required platform change.
518 + return (
519 + (windowsVersion.BuildNumber >= VMMEM_SUFFIX_NICKEL_BUILD_NUMBER) ||
520 + ((windowsVersion.BuildNumber < NICKEL_BUILD_FLOOR) && (windowsVersion.BuildNumber >= VMEMM_SUFFIX_COBALT_REFRESH_BUILD_NUMBER)) ||
521 + ((windowsVersion.BuildNumber == WindowsBuildNumbers::Cobalt) && (windowsVersion.UpdateBuildRevision >= VMMEM_SUFFIX_COBALT_RELEASE_UBR)));
522 +}
523 +
524 bool wsl::windows::common::helpers::IsWindows11OrAbove()
525 {
526 return GetWindowsVersion().BuildNumber >= WindowsBuildNumbers::Cobalt;
@@ -658,3 +712,21 @@ bool wsl::windows::common::helpers::TryAttachConsole()
712
713 return ReopenStdHandles();
714 }
715 +
716 +void wsl::windows::common::helpers::RegisterWithDcat(_In_ bool IncludeVersionNumber)
717 +try
718 +{
719 + std::wstring registeredVersion;
720 + if (IncludeVersionNumber)
721 + {
722 + registeredVersion.assign(TEXT(WSL_PACKAGE_VERSION));
723 + }
724 + else
725 + {
726 + registeredVersion.assign(L"0.0.0.0");
727 + }
728 +
729 + wil::unique_hkey dcatKey = wsl::windows::common::registry::CreateKey(HKEY_LOCAL_MACHINE, TEXT(DCAT_REGISTRATION_KEY), KEY_SET_VALUE);
730 + wsl::windows::common::registry::WriteString(dcatKey.get(), nullptr, L"Version", registeredVersion.c_str());
731 +}
732 +CATCH_LOG()
src/windows/common/helpers.hpp
+8
@@ -153,6 +153,12 @@ bool IsPackageInstalled(_In_ LPCWSTR PackageFamilyName);
153
154 bool IsServicePresent(_In_ LPCWSTR ServiceName);
155
156 +bool IsServiceRunning(_In_ LPCWSTR ServiceName);
157 +
158 +bool IsVirtioSerialConsoleSupported();
159 +
160 +bool IsVmemmSuffixSupported();
161 +
162 bool IsWindows11OrAbove();
163
164 bool IsWslOptionalComponentPresent();
@@ -193,4 +199,6 @@ void SetHandleInheritable(_In_ HANDLE Handle, _In_ bool Inheritable = true);
199
200 bool TryAttachConsole();
201
202 +void RegisterWithDcat(_In_ bool IncludeVersionNumber = true);
203 +
204 } // namespace wsl::windows::common::helpers
src/windows/common/hvsocket.cpp
+8 -6
@@ -18,8 +18,6 @@ Abstract:
18 #include "hvsocket.hpp"
19 #pragma hdrstop
20
21 -#define CONNECT_TIMEOUT (30 * 1000)
22 -
21 namespace {
22 void InitializeSocketAddress(_In_ const GUID& VmId, _In_ unsigned long Port, _Out_ PSOCKADDR_HV Address)
23 {
@@ -52,7 +50,7 @@ std::optional<wil::unique_socket> wsl::windows::common::hvsocket::CancellableAcc
50 }
51
52 wil::unique_socket wsl::windows::common::hvsocket::Connect(
55 - _In_ const GUID& VmId, _In_ unsigned long Port, _In_opt_ HANDLE ExitHandle, _In_ const std::source_location& Location)
53 + _In_ const GUID& VmId, _In_ unsigned long Port, _In_opt_ HANDLE ExitHandle, _In_opt_ ULONG Timeout, _In_ const std::source_location& Location)
54 {
55 OVERLAPPED Overlapped{};
56 const wil::unique_event OverlappedEvent(wil::EventOptions::ManualReset);
@@ -79,9 +77,10 @@ wil::unique_socket wsl::windows::common::hvsocket::Connect(
77 socket::GetResult(Socket.get(), Overlapped, INFINITE, ExitHandle, Location);
78 }
79
82 - ULONG Timeout = CONNECT_TIMEOUT;
83 - THROW_LAST_ERROR_IF(
84 - setsockopt(Socket.get(), HV_PROTOCOL_RAW, HVSOCKET_CONNECT_TIMEOUT, reinterpret_cast<char*>(&Timeout), sizeof(Timeout)) == SOCKET_ERROR);
80 + THROW_LAST_ERROR_IF_MSG(
81 + setsockopt(Socket.get(), HV_PROTOCOL_RAW, HVSOCKET_CONNECT_TIMEOUT, reinterpret_cast<char*>(&Timeout), sizeof(Timeout)) == SOCKET_ERROR,
82 + "Timeout: %lu",
83 + Timeout);
84
85 SOCKADDR_HV Addr;
86 InitializeWildcardSocketAddress(&Addr);
@@ -94,6 +93,9 @@ wil::unique_socket wsl::windows::common::hvsocket::Connect(
93 socket::GetResult(Socket.get(), Overlapped, INFINITE, ExitHandle, Location);
94 }
95
96 + // Mark the socket as connected (required to call shutdown() later).
97 + THROW_LAST_ERROR_IF(setsockopt(Socket.get(), SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, nullptr, 0) == SOCKET_ERROR);
98 +
99 return Socket;
100 }
101
src/windows/common/hvsocket.hpp
+1
@@ -29,6 +29,7 @@ wil::unique_socket Connect(
29 _In_ const GUID& VmId,
30 _In_ unsigned long Port,
31 _In_opt_ HANDLE ExitHandle = nullptr,
32 + ULONG Timeout = 30000, // TODO: Fix
33 const std::source_location& Location = std::source_location::current());
34
35 wil::unique_socket Create();
src/windows/common/precomp.h
+2
@@ -52,6 +52,7 @@ Abstract:
52 #include <msi.h>
53 #include <AccCtrl.h>
54 #include <AclAPI.h>
55 +#include <wuapi.h>
56 #include "windowsdefs.h"
57
58 // Annotations
@@ -140,6 +141,7 @@ Abstract:
141 #include <lxbusapi.h>
142
143 // Utility/helper functions
144 +#include "COMImplClass.h"
145 #include "conncheckshared.h"
146 #include "helpers.hpp"
147 #include "string.hpp"
src/windows/common/relay.cpp
+915 -3
@@ -16,14 +16,20 @@ Abstract:
16 #include "relay.hpp"
17 #pragma hdrstop
18
19 +using wsl::windows::common::relay::DockerIORelayHandle;
20 using wsl::windows::common::relay::EventHandle;
21 using wsl::windows::common::relay::HandleWrapper;
22 +using wsl::windows::common::relay::HTTPChunkBasedReadHandle;
23 using wsl::windows::common::relay::IOHandleStatus;
24 +using wsl::windows::common::relay::LineBasedReadHandle;
25 using wsl::windows::common::relay::MultiHandleWait;
26 using wsl::windows::common::relay::OverlappedIOHandle;
27 +using wsl::windows::common::relay::ReadHandle;
28 +using wsl::windows::common::relay::RelayHandle;
29 using wsl::windows::common::relay::ScopedMultiRelay;
30 using wsl::windows::common::relay::ScopedRelay;
31 using wsl::windows::common::relay::SingleAcceptHandle;
32 +using wsl::windows::common::relay::WriteHandle;
33
34 namespace {
35
@@ -383,6 +389,384 @@ void wsl::windows::common::relay::BidirectionalRelay(_In_ HANDLE LeftHandle, _In
389 }
390 }
391
392 +#define TTY_ALT_NUMPAD_VK_MENU (0x12)
393 +#define TTY_ESCAPE_CHARACTER (L'\x1b')
394 +#define TTY_INPUT_EVENT_BUFFER_SIZE (16)
395 +#define TTY_UTF8_TRANSLATION_BUFFER_SIZE (4 * TTY_INPUT_EVENT_BUFFER_SIZE)
396 +
397 +BOOL IsActionableKey(_In_ PKEY_EVENT_RECORD KeyEvent)
398 +{
399 + //
400 + // This is a bit complicated to discern.
401 + //
402 + // 1. Our first check is that we only want structures that
403 + // represent at least one key press. If we have 0, then we don't
404 + // need to bother. If we have >1, we'll send the key through
405 + // that many times into the pipe.
406 + // 2. Our second check is where it gets confusing.
407 + // a. Characters that are non-null get an automatic pass. Copy
408 + // them through to the pipe.
409 + // b. Null characters need further scrutiny. We generally do not
410 + // pass nulls through EXCEPT if they're sourced from the
411 + // virtual terminal engine (or another application living
412 + // above our layer). If they're sourced by a non-keyboard
413 + // source, they'll have no scan code (since they didn't come
414 + // from a keyboard). But that rule has an exception too:
415 + // "Enhanced keys" from above the standard range of scan
416 + // codes will return 0 also with a special flag set that says
417 + // they're an enhanced key. That means the desired behavior
418 + // is:
419 + // Scan Code = 0, ENHANCED_KEY = 0
420 + // -> This came from the VT engine or another app
421 + // above our layer.
422 + // Scan Code = 0, ENHANCED_KEY = 1
423 + // -> This came from the keyboard, but is a special
424 + // key like 'Volume Up' that wasn't generally a
425 + // part of historic (pre-1990s) keyboards.
426 + // Scan Code = <anything else>
427 + // -> This came from a keyboard directly.
428 + //
429 +
430 + if ((KeyEvent->wRepeatCount == 0) || ((KeyEvent->uChar.UnicodeChar == UNICODE_NULL) &&
431 + ((KeyEvent->wVirtualScanCode != 0) || (WI_IsFlagSet(KeyEvent->dwControlKeyState, ENHANCED_KEY)))))
432 + {
433 + return FALSE;
434 + }
435 +
436 + return TRUE;
437 +}
438 +
439 +BOOL GetNextCharacter(_In_ INPUT_RECORD* InputRecord, _Out_ PWCHAR NextCharacter)
440 +{
441 + BOOL IsNextCharacterValid = FALSE;
442 + if (InputRecord->EventType == KEY_EVENT)
443 + {
444 + const auto KeyEvent = &InputRecord->Event.KeyEvent;
445 + if ((IsActionableKey(KeyEvent) != FALSE) && ((KeyEvent->bKeyDown != FALSE) || (KeyEvent->wVirtualKeyCode == TTY_ALT_NUMPAD_VK_MENU)))
446 + {
447 + *NextCharacter = KeyEvent->uChar.UnicodeChar;
448 + IsNextCharacterValid = TRUE;
449 + }
450 + }
451 +
452 + return IsNextCharacterValid;
453 +}
454 +
455 +bool wsl::windows::common::relay::StandardInputRelay(
456 + HANDLE ConsoleHandle, HANDLE OutputHandle, const std::function<void()>& UpdateTerminalSize, HANDLE ExitEvent, const std::vector<char>& DetachSequence)
457 +{
458 + try
459 + {
460 + if (GetFileType(ConsoleHandle) != FILE_TYPE_CHAR)
461 + {
462 + wsl::windows::common::relay::InterruptableRelay(ConsoleHandle, OutputHandle, ExitEvent);
463 + return true;
464 + }
465 +
466 + //
467 + // N.B. ReadConsoleInputEx has no associated import library.
468 + //
469 +
470 + static LxssDynamicFunction<decltype(ReadConsoleInputExW)> readConsoleInput(L"Kernel32.dll", "ReadConsoleInputExW");
471 +
472 + INPUT_RECORD InputRecordBuffer[TTY_INPUT_EVENT_BUFFER_SIZE];
473 + INPUT_RECORD* InputRecordPeek = &(InputRecordBuffer[1]);
474 + KEY_EVENT_RECORD* KeyEvent;
475 + DWORD RecordsRead;
476 + OVERLAPPED Overlapped = {0};
477 + const wil::unique_event OverlappedEvent(wil::EventOptions::ManualReset);
478 + Overlapped.hEvent = OverlappedEvent.get();
479 + const HANDLE WaitHandles[] = {ExitEvent, ConsoleHandle};
480 + const std::vector<HANDLE> ExitHandles = {ExitEvent};
481 + std::deque<char> CurrentSequence;
482 +
483 + for (;;)
484 + {
485 + // Detach if the escape sequence was detected.
486 + // N.B. This needs to done at the beginning of the loop so the escape sequence is also sent to docker.
487 + if (!CurrentSequence.empty() && std::ranges::equal(CurrentSequence, DetachSequence))
488 + {
489 + return false;
490 + }
491 +
492 + //
493 + // Because some input events generated by the console are encoded with
494 + // more than one input event, we have to be smart about reading the
495 + // events.
496 + //
497 + // First, we peek at the next input event.
498 + // If it's an escape (wch == L'\x1b') event, then the characters that
499 + // follow are part of an input sequence. We can't know for sure
500 + // how long that sequence is, but we can assume it's all sent to
501 + // the input queue at once, and it's less that 16 events.
502 + // Furthermore, we can assume that if there's an Escape in those
503 + // 16 events, that the escape marks the start of a new sequence.
504 + // So, we'll peek at another 15 events looking for escapes.
505 + // If we see an escape, then we'll read one less than that,
506 + // such that the escape remains the next event in the input.
507 + // From those read events, we'll aggregate chars into a single
508 + // string to send to the subsystem.
509 + // If it's not an escape, send the event through one at a time.
510 + //
511 +
512 + //
513 + // Read one input event.
514 + //
515 +
516 + DWORD WaitStatus = (WAIT_OBJECT_0 + 1);
517 + do
518 + {
519 + THROW_IF_WIN32_BOOL_FALSE(readConsoleInput(ConsoleHandle, InputRecordBuffer, 1, &RecordsRead, CONSOLE_READ_NOWAIT));
520 +
521 + if (RecordsRead == 0)
522 + {
523 + WaitStatus = WaitForMultipleObjects(RTL_NUMBER_OF(WaitHandles), WaitHandles, false, INFINITE);
524 + }
525 + } while ((WaitStatus == (WAIT_OBJECT_0 + 1)) && (RecordsRead == 0));
526 +
527 + //
528 + // Stop processing if the exit event has been signaled.
529 + //
530 +
531 + if (WaitStatus != (WAIT_OBJECT_0 + 1))
532 + {
533 + WI_ASSERT(WaitStatus == WAIT_OBJECT_0);
534 +
535 + break;
536 + }
537 +
538 + WI_ASSERT(RecordsRead == 1);
539 +
540 + //
541 + // Don't read additional records if the first entry is a window size
542 + // event, or a repeated character. Handle those events on their own.
543 + //
544 +
545 + DWORD RecordsPeeked = 0;
546 + if ((InputRecordBuffer[0].EventType != WINDOW_BUFFER_SIZE_EVENT) &&
547 + ((InputRecordBuffer[0].EventType != KEY_EVENT) || (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount < 2)))
548 + {
549 + //
550 + // Read additional input records into the buffer if available.
551 + //
552 +
553 + THROW_IF_WIN32_BOOL_FALSE(PeekConsoleInputW(ConsoleHandle, InputRecordPeek, (RTL_NUMBER_OF(InputRecordBuffer) - 1), &RecordsPeeked));
554 + }
555 +
556 + //
557 + // Iterate over peeked records [1, RecordsPeeked].
558 + //
559 +
560 + DWORD AdditionalRecordsToRead = 0;
561 + WCHAR NextCharacter;
562 + for (DWORD RecordIndex = 1; RecordIndex <= RecordsPeeked; RecordIndex++)
563 + {
564 + if (GetNextCharacter(&InputRecordBuffer[RecordIndex], &NextCharacter) != FALSE)
565 + {
566 + KeyEvent = &InputRecordBuffer[RecordIndex].Event.KeyEvent;
567 + if (NextCharacter == TTY_ESCAPE_CHARACTER)
568 + {
569 + //
570 + // CurrentRecord is an escape event. We will start here
571 + // on the next input loop.
572 + //
573 +
574 + break;
575 + }
576 + else if (KeyEvent->wRepeatCount > 1)
577 + {
578 + //
579 + // Repeated keys are handled on their own. Start with this
580 + // key on the next input loop.
581 + //
582 +
583 + break;
584 + }
585 + else if (IS_HIGH_SURROGATE(NextCharacter) && (RecordIndex >= (RecordsPeeked - 1)))
586 + {
587 + //
588 + // If there is not enough room for the second character of
589 + // a surrogate pair, start with this character on the next
590 + // input loop.
591 + //
592 + // N.B. The test is for at least two remaining records
593 + // because typically a surrogate pair will be entered
594 + // via copy/paste, which will appear as an input
595 + // record with alt-down, alt-up and character. So to
596 + // include the next character of the surrogate pair it
597 + // is likely that the alt-up record will need to be
598 + // read first.
599 + //
600 +
601 + break;
602 + }
603 + }
604 + else if (InputRecordBuffer[RecordIndex].EventType == WINDOW_BUFFER_SIZE_EVENT)
605 + {
606 + //
607 + // A window size event is handled on its own.
608 + //
609 +
610 + break;
611 + }
612 +
613 + //
614 + // Process the additional input record.
615 + //
616 +
617 + AdditionalRecordsToRead += 1;
618 + }
619 +
620 + if (AdditionalRecordsToRead > 0)
621 + {
622 + THROW_IF_WIN32_BOOL_FALSE(
623 + readConsoleInput(ConsoleHandle, InputRecordPeek, AdditionalRecordsToRead, &RecordsRead, CONSOLE_READ_NOWAIT));
624 +
625 + if (RecordsRead == 0)
626 + {
627 + //
628 + // This would be an unexpected case. We've already peeked to see
629 + // that there are AdditionalRecordsToRead # of records in the
630 + // input that need reading, yet we didn't get them when we read.
631 + // In this case, move along and finish this input event.
632 + //
633 +
634 + break;
635 + }
636 +
637 + //
638 + // We already had one input record in the buffer before reading
639 + // additional, So account for that one too
640 + //
641 +
642 + RecordsRead += 1;
643 + }
644 +
645 + //
646 + // Process each input event. Keydowns will get aggregated into
647 + // Utf8String before getting injected into the subsystem.
648 + //
649 +
650 + WCHAR Utf16String[TTY_INPUT_EVENT_BUFFER_SIZE];
651 + ULONG Utf16StringSize = 0;
652 + COORD WindowSize{};
653 + for (DWORD RecordIndex = 0; RecordIndex < RecordsRead; RecordIndex++)
654 + {
655 + INPUT_RECORD* CurrentInputRecord = &(InputRecordBuffer[RecordIndex]);
656 + switch (CurrentInputRecord->EventType)
657 + {
658 + case KEY_EVENT:
659 +
660 + KeyEvent = &CurrentInputRecord->Event.KeyEvent;
661 +
662 + if (KeyEvent->bKeyDown && IsActionableKey(KeyEvent) && !DetachSequence.empty())
663 + {
664 + if (CurrentSequence.size() >= DetachSequence.size())
665 + {
666 + CurrentSequence.pop_front();
667 + }
668 +
669 + CurrentSequence.push_back(CurrentInputRecord->Event.KeyEvent.uChar.AsciiChar);
670 + }
671 +
672 + //
673 + // Filter out key up events unless they are from an <Alt> key.
674 + // Key up with an <Alt> key could contain a Unicode character
675 + // pasted from the clipboard and converted to an <Alt>+<Numpad> sequence.
676 + //
677 +
678 + if ((KeyEvent->bKeyDown == FALSE) && (KeyEvent->wVirtualKeyCode != TTY_ALT_NUMPAD_VK_MENU))
679 + {
680 + break;
681 + }
682 +
683 + //
684 + // Filter out key presses that are not actionable, such as just
685 + // pressing <Ctrl>, <Alt>, <Shift> etc. These key presses return
686 + // the character of null but will have a valid scan code off the
687 + // keyboard. Certain other key sequences such as Ctrl+A,
688 + // Ctrl+<space>, and Ctrl+@ will also return the character null
689 + // but have no scan code.
690 + // <Alt> + <NumPad> sequences will show an <Alt> but will have
691 + // a scancode and character specified, so they should be actionable.
692 + //
693 +
694 + if (IsActionableKey(KeyEvent) == FALSE)
695 + {
696 + break;
697 + }
698 +
699 + Utf16String[Utf16StringSize] = KeyEvent->uChar.UnicodeChar;
700 + Utf16StringSize += 1;
701 + break;
702 +
703 + case WINDOW_BUFFER_SIZE_EVENT:
704 +
705 + //
706 + // Query the window size and send an update message via the
707 + // control channel.
708 + //
709 +
710 + UpdateTerminalSize();
711 + break;
712 + }
713 + }
714 +
715 + CHAR Utf8String[TTY_UTF8_TRANSLATION_BUFFER_SIZE];
716 + DWORD Utf8StringSize = 0;
717 + if (Utf16StringSize > 0)
718 + {
719 + //
720 + // Windows uses UTF-16LE encoding, Linux uses UTF-8 by default.
721 + // Convert each UTF-16LE character into the proper UTF-8 byte
722 + // sequence equivalent.
723 + //
724 +
725 + THROW_LAST_ERROR_IF(
726 + (Utf8StringSize = WideCharToMultiByte(
727 + CP_UTF8, 0, Utf16String, Utf16StringSize, Utf8String, sizeof(Utf8String), nullptr, nullptr)) == 0);
728 + }
729 +
730 + //
731 + // Send the input bytes to the terminal.
732 + //
733 +
734 + DWORD BytesWritten = 0;
735 + const auto Utf8Span = gslhelpers::struct_as_bytes(Utf8String).first(Utf8StringSize);
736 + if ((RecordsRead == 1) && (InputRecordBuffer[0].EventType == KEY_EVENT) && (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount > 1))
737 + {
738 + WI_ASSERT(Utf16StringSize == 1);
739 +
740 + //
741 + // Handle repeated characters. They aren't part of an input
742 + // sequence, so there's only one event that's generating characters.
743 + //
744 +
745 + WORD RepeatIndex;
746 + for (RepeatIndex = 0; RepeatIndex < InputRecordBuffer[0].Event.KeyEvent.wRepeatCount; RepeatIndex += 1)
747 + {
748 + BytesWritten = wsl::windows::common::relay::InterruptableWrite(OutputHandle, Utf8Span, ExitHandles, &Overlapped);
749 + if (BytesWritten == 0)
750 + {
751 + break;
752 + }
753 + }
754 + }
755 + else if (Utf8StringSize > 0)
756 + {
757 + BytesWritten = wsl::windows::common::relay::InterruptableWrite(OutputHandle, Utf8Span, ExitHandles, &Overlapped);
758 + if (BytesWritten == 0)
759 + {
760 + break;
761 + }
762 + }
763 + }
764 + }
765 + CATCH_LOG();
766 +
767 + return true;
768 +}
769 +
770 void wsl::windows::common::relay::SocketRelay(_In_ SOCKET LeftSocket, _In_ SOCKET RightSocket, _In_ size_t BufferSize)
771 {
772 constexpr RelayFlags flags = RelayFlags::LeftIsSocket | RelayFlags::RightIsSocket;
@@ -602,9 +986,9 @@ bool MultiHandleWait::Run(std::optional<std::chrono::milliseconds> Timeout)
986 while (!m_handles.empty() && !m_cancel)
987 {
988 // Schedule IO on each handle until all are either pending, or completed.
605 - for (size_t i = 0; i < m_handles.size(); i++)
989 + for (size_t i = 0; i < m_handles.size() && !m_cancel; i++)
990 {
607 - while (m_handles[i].second->GetState() == IOHandleStatus::Standby)
991 + while (m_handles[i].second->GetState() == IOHandleStatus::Standby && !m_cancel)
992 {
993 try
994 {
@@ -615,6 +999,7 @@ bool MultiHandleWait::Run(std::optional<std::chrono::milliseconds> Timeout)
999 if (WI_IsFlagSet(m_handles[i].first, Flags::IgnoreErrors))
1000 {
1001 m_handles[i].second.reset(); // Reset the handle so it can be deleted.
1002 + break;
1003 }
1004 else
1005 {
@@ -625,6 +1010,7 @@ bool MultiHandleWait::Run(std::optional<std::chrono::milliseconds> Timeout)
1010 }
1011
1012 // Remove completed handles from m_handles.
1013 + bool hasHandleToWaitFor = false;
1014 for (auto it = m_handles.begin(); it != m_handles.end();)
1015 {
1016 if (!it->second)
@@ -642,11 +1028,16 @@ bool MultiHandleWait::Run(std::optional<std::chrono::milliseconds> Timeout)
1028 }
1029 else
1030 {
1031 + // If only NeedNotComplete handles are left, we want to exit Run.
1032 + if (WI_IsFlagClear(it->first, Flags::NeedNotComplete))
1033 + {
1034 + hasHandleToWaitFor = true;
1035 + }
1036 ++it;
1037 }
1038 }
1039
649 - if (m_handles.empty() || m_cancel)
1040 + if (!hasHandleToWaitFor || m_cancel)
1041 {
1042 break;
1043 }
@@ -727,6 +1118,113 @@ HANDLE EventHandle::GetHandle() const
1118 return Handle.Get();
1119 }
1120
1121 +ReadHandle::ReadHandle(HandleWrapper&& MovedHandle, std::function<void(const gsl::span<char>& Buffer)>&& OnRead) :
1122 + Handle(std::move(MovedHandle)), OnRead(OnRead), Offset(InitializeFileOffset(Handle.Get()))
1123 +{
1124 + Overlapped.hEvent = Event.get();
1125 +}
1126 +
1127 +ReadHandle::~ReadHandle()
1128 +{
1129 + if (State == IOHandleStatus::Pending)
1130 + {
1131 + DWORD bytesRead{};
1132 + if (CancelIoEx(Handle.Get(), &Overlapped))
1133 + {
1134 + if (!GetOverlappedResult(Handle.Get(), &Overlapped, &bytesRead, true))
1135 + {
1136 + auto error = GetLastError();
1137 + LOG_LAST_ERROR_IF(error != ERROR_CONNECTION_ABORTED && error != ERROR_OPERATION_ABORTED);
1138 + }
1139 + }
1140 + else
1141 + {
1142 + // ERROR_NOT_FOUND is returned if there was no IO to cancel.
1143 + LOG_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND);
1144 + }
1145 + }
1146 +}
1147 +
1148 +void ReadHandle::Schedule()
1149 +{
1150 + WI_ASSERT(State == IOHandleStatus::Standby);
1151 +
1152 + Event.ResetEvent();
1153 +
1154 + // Schedule the read.
1155 + DWORD bytesRead{};
1156 + Overlapped.Offset = Offset.LowPart;
1157 + Overlapped.OffsetHigh = Offset.HighPart;
1158 + if (ReadFile(Handle.Get(), Buffer.data(), static_cast<DWORD>(Buffer.size()), &bytesRead, &Overlapped))
1159 + {
1160 + Offset.QuadPart += bytesRead;
1161 +
1162 + // Signal the read.
1163 + OnRead(gsl::make_span<char>(Buffer.data(), static_cast<size_t>(bytesRead)));
1164 +
1165 + // ReadFile completed immediately, process the result right away.
1166 + if (bytesRead == 0)
1167 + {
1168 + State = IOHandleStatus::Completed;
1169 + return; // Handle is completely read, don't try again.
1170 + }
1171 +
1172 + // Read was done synchronously, remain in 'standby' state.
1173 + }
1174 + else
1175 + {
1176 + auto error = GetLastError();
1177 + if (error == ERROR_HANDLE_EOF || error == ERROR_BROKEN_PIPE)
1178 + {
1179 + // Signal an empty read for EOF.
1180 + OnRead({});
1181 +
1182 + State = IOHandleStatus::Completed;
1183 + return;
1184 + }
1185 +
1186 + THROW_LAST_ERROR_IF_MSG(error != ERROR_IO_PENDING, "Handle: 0x%p", (void*)Handle.Get());
1187 +
1188 + // The read is pending, update to 'Pending'
1189 + State = IOHandleStatus::Pending;
1190 + }
1191 +}
1192 +
1193 +void ReadHandle::Collect()
1194 +{
1195 + WI_ASSERT(State == IOHandleStatus::Pending);
1196 +
1197 + // Transition back to standby
1198 + State = IOHandleStatus::Standby;
1199 +
1200 + // Complete the read.
1201 + DWORD bytesRead{};
1202 + if (!GetOverlappedResult(Handle.Get(), &Overlapped, &bytesRead, false))
1203 + {
1204 + auto error = GetLastError();
1205 + THROW_WIN32_IF(error, error != ERROR_HANDLE_EOF && error != ERROR_BROKEN_PIPE);
1206 +
1207 + // We received ERROR_HANDLE_EOF or ERROR_BROKEN_PIPE. Validate that this was indeed a zero byte read.
1208 + WI_ASSERT(bytesRead == 0);
1209 + }
1210 +
1211 + Offset.QuadPart += bytesRead;
1212 +
1213 + // Signal the read.
1214 + OnRead(gsl::make_span<char>(Buffer.data(), static_cast<size_t>(bytesRead)));
1215 +
1216 + // Transition to Complete if this was a zero byte read.
1217 + if (bytesRead == 0)
1218 + {
1219 + State = IOHandleStatus::Completed;
1220 + }
1221 +}
1222 +
1223 +HANDLE ReadHandle::GetHandle() const
1224 +{
1225 + return Event.get();
1226 +}
1227 +
1228 SingleAcceptHandle::SingleAcceptHandle(HandleWrapper&& ListenSocket, HandleWrapper&& AcceptedSocket, std::function<void()>&& OnAccepted) :
1229 ListenSocket(std::move(ListenSocket)), AcceptedSocket(std::move(AcceptedSocket)), OnAccepted(std::move(OnAccepted))
1230 {
@@ -786,4 +1284,418 @@ void SingleAcceptHandle::Collect()
1284 HANDLE SingleAcceptHandle::GetHandle() const
1285 {
1286 return Event.get();
1287 +}
1288 +
1289 +LineBasedReadHandle::LineBasedReadHandle(HandleWrapper&& Handle, std::function<void(const gsl::span<char>& Line)>&& OnLine, bool Crlf) :
1290 + ReadHandle(std::move(Handle), [this](const gsl::span<char>& Buffer) { OnRead(Buffer); }), OnLine(OnLine), Crlf(Crlf)
1291 +{
1292 +}
1293 +
1294 +LineBasedReadHandle::~LineBasedReadHandle()
1295 +{
1296 + // N.B. PendingBuffer can contain remaining data is an exception was thrown during parsing.
1297 +}
1298 +
1299 +void LineBasedReadHandle::OnRead(const gsl::span<char>& Buffer)
1300 +{
1301 + // If we reach of the end, signal a line with the remaining buffer.
1302 + if (Buffer.empty() && !PendingBuffer.empty())
1303 + {
1304 + OnLine(PendingBuffer);
1305 + PendingBuffer.clear();
1306 + return;
1307 + }
1308 +
1309 + auto begin = Buffer.begin();
1310 + auto end = std::ranges::find(Buffer, Crlf ? '\r' : '\n');
1311 + while (end != Buffer.end())
1312 + {
1313 + if (Crlf)
1314 + {
1315 + end++; // Move to the following '\n'
1316 +
1317 + if (end == Buffer.end() || *end != '\n') // Incomplete CRLF sequence. Append to buffer and continue.
1318 + {
1319 + PendingBuffer.insert(PendingBuffer.end(), begin, end);
1320 + begin = end;
1321 + end = std::ranges::find(end, Buffer.end(), '\r');
1322 + continue;
1323 + }
1324 + }
1325 +
1326 + // Discard the '\r' in CRLF mode.
1327 + PendingBuffer.insert(PendingBuffer.end(), begin, Crlf ? end - 1 : end);
1328 +
1329 + if (!PendingBuffer.empty())
1330 + {
1331 + OnLine(PendingBuffer);
1332 + PendingBuffer.clear();
1333 + }
1334 +
1335 + begin = end + 1;
1336 + end = std::ranges::find(begin, Buffer.end(), Crlf ? '\r' : '\n');
1337 + }
1338 +
1339 + PendingBuffer.insert(PendingBuffer.end(), begin, end);
1340 +}
1341 +
1342 +HTTPChunkBasedReadHandle::HTTPChunkBasedReadHandle(HandleWrapper&& MovedHandle, std::function<void(const gsl::span<char>& Line)>&& OnChunk) :
1343 + ReadHandle(std::move(MovedHandle), [this](const gsl::span<char>& Buffer) { OnRead(Buffer); }), OnChunk(std::move(OnChunk))
1344 +{
1345 +}
1346 +
1347 +HTTPChunkBasedReadHandle::~HTTPChunkBasedReadHandle()
1348 +{
1349 + // N.B. PendingBuffer can contain remaining data is an exception was thrown during parsing.
1350 + LOG_HR_IF(E_UNEXPECTED, !PendingBuffer.empty() || PendingChunkSize != 0 || ExpectHeader);
1351 +}
1352 +
1353 +void HTTPChunkBasedReadHandle::OnRead(const gsl::span<char>& Input)
1354 +{
1355 + // See: https://httpwg.org/specs/rfc9112.html#field.transfer-encoding
1356 +
1357 + if (Input.empty())
1358 + {
1359 + // N.B. The body can be terminated by a zero-length chunk.
1360 + THROW_HR_IF(E_INVALIDARG, PendingChunkSize != 0 || ExpectHeader);
1361 + }
1362 +
1363 + auto buffer = Input;
1364 +
1365 + auto advance = [&](size_t count) {
1366 + WI_ASSERT(buffer.size() >= count);
1367 + buffer = buffer.subspan(count);
1368 + };
1369 +
1370 + while (!buffer.empty())
1371 + {
1372 + if (PendingChunkSize == 0)
1373 + {
1374 + // Consume CRLF's between chunks.
1375 + if (PendingBuffer.empty() && (buffer.front() == '\r' || buffer.front() == '\n'))
1376 + {
1377 + advance(1);
1378 + continue;
1379 + }
1380 +
1381 + ExpectHeader = true;
1382 +
1383 + auto end = std::ranges::find(buffer, '\n');
1384 + PendingBuffer.insert(PendingBuffer.end(), buffer.begin(), end);
1385 + if (end == buffer.end())
1386 + {
1387 + // Incomplete size header, buffer until next read.
1388 + break;
1389 + }
1390 + // Advance beyond the LF
1391 + advance(end - buffer.begin() + 1);
1392 +
1393 + THROW_HR_IF_MSG(
1394 + E_INVALIDARG,
1395 + PendingBuffer.size() < 2 || PendingBuffer.back() != '\r',
1396 + "Malformed chunk header: %hs",
1397 + PendingBuffer.c_str());
1398 + PendingBuffer.erase(PendingBuffer.end() - 1, PendingBuffer.end()); // Remove CR.
1399 +
1400 +#ifdef WSLC_HTTP_DEBUG
1401 +
1402 + WSL_LOG("HTTPChunkHeader", TraceLoggingValue(PendingBuffer.c_str(), "Size"));
1403 +
1404 +#endif
1405 +
1406 + try
1407 + {
1408 + size_t parsed{};
1409 + PendingChunkSize = std::stoul(PendingBuffer.c_str(), &parsed, 16);
1410 + THROW_HR_IF(E_INVALIDARG, parsed != PendingBuffer.size());
1411 + }
1412 + catch (...)
1413 + {
1414 + THROW_HR_MSG(E_INVALIDARG, "Failed to parse chunk size: %hs", PendingBuffer.c_str());
1415 + }
1416 +
1417 + ExpectHeader = false;
1418 + PendingBuffer.clear();
1419 + }
1420 + else
1421 + {
1422 + // Consume the chunk.
1423 + auto consumedBytes = std::min(PendingChunkSize, buffer.size());
1424 + PendingBuffer.append(buffer.data(), consumedBytes);
1425 + advance(consumedBytes);
1426 +
1427 + WI_ASSERT(PendingChunkSize >= consumedBytes);
1428 + PendingChunkSize -= consumedBytes;
1429 +
1430 + if (PendingChunkSize == 0)
1431 + {
1432 +
1433 +#ifdef WSLC_HTTP_DEBUG
1434 +
1435 + WSL_LOG("HTTPChunk", TraceLoggingValue(PendingBuffer.c_str(), "Content"));
1436 +
1437 +#endif
1438 + OnChunk(PendingBuffer);
1439 + PendingBuffer.clear();
1440 + }
1441 + }
1442 + }
1443 +}
1444 +
1445 +WriteHandle::WriteHandle(HandleWrapper&& MovedHandle, const std::vector<char>& Buffer) :
1446 + Handle(std::move(MovedHandle)), Buffer(Buffer), Offset(InitializeFileOffset(Handle.Get()))
1447 +{
1448 + Overlapped.hEvent = Event.get();
1449 +}
1450 +
1451 +WriteHandle::~WriteHandle()
1452 +{
1453 + if (State == IOHandleStatus::Pending)
1454 + {
1455 + DWORD bytesRead{};
1456 + if (CancelIoEx(Handle.Get(), &Overlapped))
1457 + {
1458 + if (!GetOverlappedResult(Handle.Get(), &Overlapped, &bytesRead, true))
1459 + {
1460 + auto error = GetLastError();
1461 + LOG_LAST_ERROR_IF(error != ERROR_CONNECTION_ABORTED && error != ERROR_OPERATION_ABORTED);
1462 + }
1463 + }
1464 + else
1465 + {
1466 + // ERROR_NOT_FOUND is returned if there was no IO to cancel.
1467 + LOG_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND);
1468 + }
1469 + }
1470 +}
1471 +
1472 +void WriteHandle::Schedule()
1473 +{
1474 + WI_ASSERT(State == IOHandleStatus::Standby);
1475 +
1476 + Event.ResetEvent();
1477 +
1478 + Overlapped.Offset = Offset.LowPart;
1479 + Overlapped.OffsetHigh = Offset.HighPart;
1480 +
1481 + // Schedule the write.
1482 + DWORD bytesWritten{};
1483 + if (WriteFile(Handle.Get(), Buffer.data(), static_cast<DWORD>(Buffer.size()), &bytesWritten, &Overlapped))
1484 + {
1485 + Offset.QuadPart += bytesWritten;
1486 +
1487 + Buffer.erase(Buffer.begin(), Buffer.begin() + bytesWritten);
1488 + if (Buffer.empty())
1489 + {
1490 + State = IOHandleStatus::Completed;
1491 + }
1492 + }
1493 + else
1494 + {
1495 + auto error = GetLastError();
1496 + THROW_LAST_ERROR_IF_MSG(error != ERROR_IO_PENDING, "Handle: 0x%p", (void*)Handle.Get());
1497 +
1498 + // The write is pending, update to 'Pending'
1499 + State = IOHandleStatus::Pending;
1500 + }
1501 +}
1502 +
1503 +void WriteHandle::Collect()
1504 +{
1505 + WI_ASSERT(State == IOHandleStatus::Pending);
1506 +
1507 + // Transition back to standby
1508 + State = IOHandleStatus::Standby;
1509 +
1510 + // Complete the write.
1511 + DWORD bytesWritten{};
1512 + THROW_IF_WIN32_BOOL_FALSE(GetOverlappedResult(Handle.Get(), &Overlapped, &bytesWritten, false));
1513 + Offset.QuadPart += bytesWritten;
1514 +
1515 + Buffer.erase(Buffer.begin(), Buffer.begin() + bytesWritten);
1516 + if (Buffer.empty())
1517 + {
1518 + State = IOHandleStatus::Completed;
1519 + }
1520 +}
1521 +
1522 +void WriteHandle::Push(const gsl::span<char>& Content)
1523 +{
1524 + // Don't write if a WriteFile() is pending, since that could cause the buffer to reallocate.
1525 + WI_ASSERT(State == IOHandleStatus::Standby || State == IOHandleStatus::Completed);
1526 + WI_ASSERT(!Content.empty());
1527 +
1528 + Buffer.insert(Buffer.end(), Content.begin(), Content.end());
1529 +
1530 + State = IOHandleStatus::Standby;
1531 +}
1532 +
1533 +HANDLE WriteHandle::GetHandle() const
1534 +{
1535 + return Event.get();
1536 +}
1537 +
1538 +DockerIORelayHandle::DockerIORelayHandle(HandleWrapper&& ReadHandle, HandleWrapper&& Stdout, HandleWrapper&& Stderr, Format ReadFormat) :
1539 + WriteStdout(std::move(Stdout)), WriteStderr(std::move(Stderr))
1540 +{
1541 + if (ReadFormat == Format::HttpChunked)
1542 + {
1543 + Read = std::make_unique<HTTPChunkBasedReadHandle>(
1544 + std::move(ReadHandle), [this](const gsl::span<char>& Line) { this->OnRead(Line); });
1545 + }
1546 + else
1547 + {
1548 + Read = std::make_unique<relay::ReadHandle>(
1549 + std::move(ReadHandle), [this](const gsl::span<char>& Buffer) { this->OnRead(Buffer); });
1550 + }
1551 +}
1552 +
1553 +void DockerIORelayHandle::Schedule()
1554 +{
1555 + WI_ASSERT(State == IOHandleStatus::Standby);
1556 + WI_ASSERT(Read->GetState() != IOHandleStatus::Pending);
1557 +
1558 + // If we have an active handle and a buffer, try to flush that first.
1559 + if (ActiveHandle != nullptr && !PendingBuffer.empty())
1560 + {
1561 + // Push the data to the selected handle.
1562 + DWORD bytesToWrite = std::min(static_cast<DWORD>(RemainingBytes), static_cast<DWORD>(PendingBuffer.size()));
1563 +
1564 + ActiveHandle->Push(gsl::make_span(PendingBuffer.data(), bytesToWrite));
1565 +
1566 + // Consume the written bytes.
1567 + RemainingBytes -= bytesToWrite;
1568 + PendingBuffer.erase(PendingBuffer.begin(), PendingBuffer.begin() + bytesToWrite);
1569 +
1570 + // Schedule the write.
1571 + ActiveHandle->Schedule();
1572 +
1573 + // If the write is pending, update to 'Pending'
1574 + if (ActiveHandle->GetState() == IOHandleStatus::Pending)
1575 + {
1576 + State = IOHandleStatus::Pending;
1577 + }
1578 + else if (ActiveHandle->GetState() == IOHandleStatus::Completed)
1579 + {
1580 + if (RemainingBytes == 0)
1581 + {
1582 + // Switch back to reading if we've written all bytes for this chunk.
1583 + ActiveHandle = nullptr;
1584 +
1585 + ProcessNextHeader();
1586 + }
1587 + }
1588 + }
1589 + else
1590 + {
1591 + if (Read->GetState() == IOHandleStatus::Completed)
1592 + {
1593 + LOG_HR_IF(E_UNEXPECTED, ActiveHandle != nullptr);
1594 +
1595 + // No more data to read, we're done.
1596 + State = IOHandleStatus::Completed;
1597 + return;
1598 + }
1599 +
1600 + // Schedule a read from the input.
1601 + Read->Schedule();
1602 + if (Read->GetState() == IOHandleStatus::Pending)
1603 + {
1604 + State = IOHandleStatus::Pending;
1605 + }
1606 + }
1607 +}
1608 +
1609 +void DockerIORelayHandle::Collect()
1610 +{
1611 + WI_ASSERT(State == IOHandleStatus::Pending);
1612 +
1613 + if (ActiveHandle != nullptr && ActiveHandle->GetState() == IOHandleStatus::Pending)
1614 + {
1615 + // Complete the write.
1616 + ActiveHandle->Collect();
1617 +
1618 + // If the write is completed, switch back to reading.
1619 + if (RemainingBytes == 0)
1620 + {
1621 + if (ActiveHandle->GetState() == IOHandleStatus::Completed)
1622 + {
1623 + ActiveHandle = nullptr;
1624 + }
1625 + }
1626 +
1627 + // Transition back to standby if there's still data to read.
1628 + // Otherwise switch to Completed since everything is done.
1629 + if (Read->GetState() == IOHandleStatus::Completed)
1630 + {
1631 + LOG_HR_IF(E_UNEXPECTED, RemainingBytes != 0);
1632 +
1633 + State = IOHandleStatus::Completed;
1634 + }
1635 + else
1636 + {
1637 + State = IOHandleStatus::Standby;
1638 + }
1639 + }
1640 + else
1641 + {
1642 + WI_ASSERT(Read->GetState() == IOHandleStatus::Pending);
1643 +
1644 + // Complete the read.
1645 + Read->Collect();
1646 +
1647 + // Transition back to standby.
1648 + State = IOHandleStatus::Standby;
1649 + }
1650 +}
1651 +
1652 +HANDLE DockerIORelayHandle::GetHandle() const
1653 +{
1654 + if (ActiveHandle != nullptr && ActiveHandle->GetState() == IOHandleStatus::Pending)
1655 + {
1656 + return ActiveHandle->GetHandle();
1657 + }
1658 + else
1659 + {
1660 + return Read->GetHandle();
1661 + }
1662 +}
1663 +
1664 +void DockerIORelayHandle::ProcessNextHeader()
1665 +{
1666 + if (PendingBuffer.size() < sizeof(MultiplexedHeader))
1667 + {
1668 + // Not enough data for a header yet.
1669 + return;
1670 + }
1671 +
1672 + const auto* header = reinterpret_cast<const MultiplexedHeader*>(PendingBuffer.data());
1673 + RemainingBytes = ntohl(header->Length);
1674 +
1675 + if (header->Fd == 1)
1676 + {
1677 + ActiveHandle = &WriteStdout;
1678 + }
1679 + else if (header->Fd == 2)
1680 + {
1681 + ActiveHandle = &WriteStderr;
1682 + }
1683 + else
1684 + {
1685 + THROW_HR_MSG(E_INVALIDARG, "Invalid Docker IO multiplexed header fd: %u", header->Fd);
1686 + }
1687 +
1688 + // Consume the header.
1689 + PendingBuffer.erase(PendingBuffer.begin(), PendingBuffer.begin() + sizeof(MultiplexedHeader));
1690 +}
1691 +
1692 +void DockerIORelayHandle::OnRead(const gsl::span<char>& Buffer)
1693 +{
1694 + PendingBuffer.insert(PendingBuffer.end(), Buffer.begin(), Buffer.end());
1695 +
1696 + if (ActiveHandle == nullptr)
1697 + {
1698 + // If no handle is active, expect a header.
1699 + ProcessNextHeader();
1700 + }
1701 }
\ No newline at end of file
src/windows/common/relay.hpp
+221 -1
@@ -43,6 +43,13 @@ bool InterruptableWait(_In_ HANDLE WaitObject, _In_ const std::vector<HANDLE>& E
43 DWORD
44 InterruptableWrite(_In_ HANDLE OutputHandle, _In_ gsl::span<const gsl::byte> Buffer, _In_ const std::vector<HANDLE>& ExitHandles, _In_ LPOVERLAPPED Overlapped);
45
46 +bool StandardInputRelay(
47 + HANDLE ConsoleHandle,
48 + HANDLE OutputHandle,
49 + const std::function<void()>& UpdateTerminalSize,
50 + HANDLE ExitEvent,
51 + const std::vector<char>& DetachSequence = {});
52 +
53 enum class RelayFlags
54 {
55 None = 0,
@@ -257,6 +264,28 @@ private:
264 std::function<void()> OnSignalled;
265 };
266
267 +class ReadHandle : public OverlappedIOHandle
268 +{
269 +public:
270 + NON_COPYABLE(ReadHandle);
271 + NON_MOVABLE(ReadHandle);
272 +
273 + ReadHandle(HandleWrapper&& MovedHandle, std::function<void(const gsl::span<char>& Buffer)>&& OnRead);
274 + virtual ~ReadHandle();
275 +
276 + void Schedule() override;
277 + void Collect() override;
278 + HANDLE GetHandle() const override;
279 +
280 +private:
281 + HandleWrapper Handle;
282 + std::function<void(const gsl::span<char>& Buffer)> OnRead;
283 + wil::unique_event Event{wil::EventOptions::ManualReset};
284 + OVERLAPPED Overlapped{};
285 + std::vector<char> Buffer = std::vector<char>(LX_RELAY_BUFFER_SIZE);
286 + LARGE_INTEGER Offset{};
287 +};
288 +
289 class SingleAcceptHandle : public OverlappedIOHandle
290 {
291 public:
@@ -279,14 +308,205 @@ private:
308 char AcceptBuffer[2 * sizeof(SOCKADDR_STORAGE)];
309 };
310
311 +class LineBasedReadHandle : public ReadHandle
312 +{
313 +public:
314 + NON_COPYABLE(LineBasedReadHandle);
315 + NON_MOVABLE(LineBasedReadHandle);
316 +
317 + LineBasedReadHandle(HandleWrapper&& Handle, std::function<void(const gsl::span<char>& Buffer)>&& OnLine, bool Crlf);
318 + ~LineBasedReadHandle();
319 +
320 +private:
321 + void OnRead(const gsl::span<char>& Buffer);
322 +
323 + std::function<void(const gsl::span<char>& Buffer)> OnLine;
324 + std::string PendingBuffer;
325 + bool Crlf{};
326 +};
327 +
328 +class HTTPChunkBasedReadHandle : public ReadHandle
329 +{
330 +public:
331 + NON_COPYABLE(HTTPChunkBasedReadHandle);
332 + NON_MOVABLE(HTTPChunkBasedReadHandle);
333 +
334 + HTTPChunkBasedReadHandle(HandleWrapper&& Handler, std::function<void(const gsl::span<char>& Buffer)>&& OnChunk);
335 + ~HTTPChunkBasedReadHandle();
336 +
337 + void OnRead(const gsl::span<char>& Line);
338 +
339 +private:
340 + std::function<void(const gsl::span<char>& Buffer)> OnChunk;
341 + std::string PendingBuffer;
342 + uint64_t PendingChunkSize = 0;
343 + bool ExpectHeader = true;
344 +};
345 +
346 +class WriteHandle : public OverlappedIOHandle
347 +{
348 +public:
349 + NON_COPYABLE(WriteHandle);
350 + NON_MOVABLE(WriteHandle);
351 +
352 + WriteHandle(HandleWrapper&& Handle, const std::vector<char>& Buffer = {});
353 + ~WriteHandle();
354 + void Schedule() override;
355 + void Collect() override;
356 + HANDLE GetHandle() const override;
357 + void Push(const gsl::span<char>& Buffer);
358 +
359 +private:
360 + HandleWrapper Handle;
361 + wil::unique_event Event{wil::EventOptions::ManualReset};
362 + OVERLAPPED Overlapped{};
363 + std::vector<char> Buffer;
364 + LARGE_INTEGER Offset{};
365 +};
366 +
367 +template <typename TRead = ReadHandle>
368 +class RelayHandle : public OverlappedIOHandle
369 +{
370 +public:
371 + NON_COPYABLE(RelayHandle);
372 + NON_MOVABLE(RelayHandle);
373 +
374 + RelayHandle(HandleWrapper&& Input, HandleWrapper&& Output) :
375 + Read(std::move(Input), [this](const gsl::span<char>& Buffer) { return OnRead(Buffer); }), Write(std::move(Output))
376 + {
377 + }
378 +
379 + void Schedule() override
380 + {
381 + WI_ASSERT(State == IOHandleStatus::Standby);
382 +
383 + // If the Buffer is empty, then we're reading.
384 + if (PendingBuffer.empty())
385 + {
386 + // If the output buffer is empty and the reading end is completed, then we're done.
387 + if (Read.GetState() == IOHandleStatus::Completed)
388 + {
389 + State = IOHandleStatus::Completed;
390 + return;
391 + }
392 +
393 + Read.Schedule();
394 +
395 + // If the read is pending, update to 'Pending'
396 + if (Read.GetState() == IOHandleStatus::Pending)
397 + {
398 + State = IOHandleStatus::Pending;
399 + }
400 + }
401 + else
402 + {
403 + Write.Push(PendingBuffer);
404 + PendingBuffer.clear();
405 +
406 + Write.Schedule();
407 +
408 + if (Write.GetState() == IOHandleStatus::Pending)
409 + {
410 + // The write is pending, update to 'Pending'
411 + State = IOHandleStatus::Pending;
412 + }
413 + }
414 + }
415 +
416 + void Collect() override
417 + {
418 + WI_ASSERT(State == IOHandleStatus::Pending);
419 +
420 + // Transition back to standby
421 + State = IOHandleStatus::Standby;
422 +
423 + if (Read.GetState() == IOHandleStatus::Pending)
424 + {
425 + Read.Collect();
426 + }
427 + else
428 + {
429 + WI_ASSERT(Write.GetState() == IOHandleStatus::Pending);
430 + Write.Collect();
431 + }
432 + }
433 +
434 + HANDLE GetHandle() const override
435 + {
436 + if (Read.GetState() == IOHandleStatus::Pending)
437 + {
438 + return Read.GetHandle();
439 + }
440 + else
441 + {
442 + WI_ASSERT(Write.GetState() == IOHandleStatus::Pending);
443 + return Write.GetHandle();
444 + }
445 + }
446 +
447 +private:
448 + void OnRead(const gsl::span<char>& Content)
449 + {
450 + PendingBuffer.insert(PendingBuffer.end(), Content.begin(), Content.end());
451 + }
452 +
453 + TRead Read;
454 + WriteHandle Write;
455 + std::vector<char> PendingBuffer;
456 +};
457 +
458 +class DockerIORelayHandle : public OverlappedIOHandle
459 +{
460 +public:
461 + NON_COPYABLE(DockerIORelayHandle);
462 + NON_MOVABLE(DockerIORelayHandle);
463 +
464 + enum class Format
465 + {
466 + Raw,
467 + HttpChunked
468 + };
469 +
470 + DockerIORelayHandle(HandleWrapper&& Input, HandleWrapper&& Stdout, HandleWrapper&& Stderr, Format ReadFormat);
471 + void Schedule() override;
472 + void Collect() override;
473 + HANDLE GetHandle() const override;
474 +
475 +#pragma pack(push, 1)
476 + struct MultiplexedHeader
477 + {
478 + uint8_t Fd;
479 + char Zeroes[3];
480 + uint32_t Length;
481 + };
482 +#pragma pack(pop)
483 +
484 + static_assert(sizeof(MultiplexedHeader) == 8);
485 +
486 +private:
487 + void OnRead(const gsl::span<char>& Buffer);
488 + void ProcessNextHeader();
489 +
490 + std::unique_ptr<OverlappedIOHandle> Read;
491 + WriteHandle WriteStdout;
492 + WriteHandle WriteStderr;
493 + std::vector<char> PendingBuffer;
494 + WriteHandle* ActiveHandle = nullptr;
495 + size_t RemainingBytes = 0;
496 +};
497 +
498 class MultiHandleWait
499 {
500 public:
501 + NON_COPYABLE(MultiHandleWait);
502 + DEFAULT_MOVABLE(MultiHandleWait);
503 +
504 enum Flags
505 {
506 None = 0,
507 CancelOnCompleted = 1,
289 - IgnoreErrors = 2
508 + IgnoreErrors = 2,
509 + NeedNotComplete = 4,
510 };
511
512 MultiHandleWait() = default;
src/windows/common/string.cpp
+78 -1
@@ -190,6 +190,73 @@ std::wstring wsl::windows::common::string::BytesToHex(const std::vector<BYTE>& b
190 return str.str();
191 }
192
193 +namespace {
194 +bool IsHexSpecifier(char first, char second)
195 +{
196 + return first == '0' && tolower(static_cast<unsigned char>(second)) == 'x';
197 +}
198 +
199 +bool IsHexSpecifier(wchar_t first, wchar_t second)
200 +{
201 + return first == L'0' && towlower(second) == L'x';
202 +}
203 +
204 +BYTE ConvertHexByte(const char* hex, char** endPtr)
205 +{
206 + return static_cast<BYTE>(strtoul(hex, endPtr, 16));
207 +}
208 +
209 +BYTE ConvertHexByte(const wchar_t* hex, wchar_t** endPtr)
210 +{
211 + return static_cast<BYTE>(wcstoul(hex, endPtr, 16));
212 +}
213 +
214 +template <typename T>
215 +std::vector<BYTE> HexToBytesT(std::basic_string_view<T> input)
216 +{
217 + if (input.length() % 2 != 0)
218 + {
219 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageInvalidHexString(std::basic_string<T>{input}));
220 + }
221 +
222 + std::vector<BYTE> result;
223 + result.reserve(input.length() / 2);
224 + T currentHex[3]{};
225 + for (size_t i = 0; i < input.size(); i += 2)
226 + {
227 + // Skip '0x', if any
228 + if (i == 0 && IsHexSpecifier(input[0], input[1]))
229 + {
230 + continue;
231 + }
232 +
233 + currentHex[0] = input[i];
234 + currentHex[1] = input[i + 1];
235 + T* endPtr{};
236 +
237 + const auto byte = ConvertHexByte(currentHex, &endPtr);
238 + if (endPtr != currentHex + 2)
239 + {
240 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageInvalidHexString(std::basic_string<T>{input}));
241 + }
242 +
243 + result.push_back(byte);
244 + }
245 +
246 + return result;
247 +}
248 +} // namespace
249 +
250 +std::vector<BYTE> wsl::windows::common::string::HexToBytes(std::string_view input)
251 +{
252 + return HexToBytesT(input);
253 +}
254 +
255 +std::vector<BYTE> wsl::windows::common::string::HexToBytes(std::wstring_view input)
256 +{
257 + return HexToBytesT(input);
258 +}
259 +
260 std::string wsl::windows::common::string::WideToMultiByte(_In_opt_ LPCWSTR Source, _In_ size_t CharacterCount)
261 {
262 if (CharacterCount == -1)
@@ -217,4 +284,14 @@ std::string wsl::windows::common::string::WideToMultiByte(_In_opt_ LPCWSTR Sourc
284 std::string wsl::windows::common::string::WideToMultiByte(_In_ std::wstring_view Source)
285 {
286 return WideToMultiByte(Source.data(), Source.length());
220 -}
\ No newline at end of file
287 +}
288 +
289 +std::wstring wsl::windows::common::string::TruncateId(_In_ std::wstring_view id, bool shortenLength)
290 +{
291 + return TruncateIdImpl(id, shortenLength);
292 +}
293 +
294 +std::string wsl::windows::common::string::TruncateId(_In_ std::string_view id, bool shortenLength)
295 +{
296 + return TruncateIdImpl(id, shortenLength);
297 +}
src/windows/common/string.hpp
+29
@@ -41,11 +41,40 @@ std::wstring SockAddrInetToWstring(const SOCKADDR_INET& sockAddrInet);
41 std::wstring IntegerIpv4ToWstring(const uint32_t ipAddress);
42 SOCKADDR_INET StringToSockAddrInet(const std::wstring& stringIpAddress);
43 std::wstring BytesToHex(const std::vector<BYTE>& bytes);
44 +std::vector<BYTE> HexToBytes(std::string_view input);
45 +std::vector<BYTE> HexToBytes(std::wstring_view input);
46
47 std::string WideToMultiByte(_In_opt_ LPCWSTR Source, _In_ size_t CharacterCount = -1);
48
49 std::string WideToMultiByte(_In_ std::wstring_view Source);
50
51 +std::wstring TruncateId(_In_ std::wstring_view id, bool shortenLength = true);
52 +std::string TruncateId(_In_ std::string_view id, bool shortenLength = true);
53 +
54 +// Template implementation for TruncateId to avoid code duplication.
55 +// Algorithm inspired from Moby for consistency in presentation of shortened IDs.
56 +// Always strips the algorithm prefix (e.g., "sha256:") if present, and optionally shortens to 12 characters.
57 +template <typename TChar>
58 +inline std::basic_string<TChar> TruncateIdImpl(std::basic_string_view<TChar> id, bool shortenLength)
59 +{
60 + constexpr size_t shortLen = 12;
61 + constexpr TChar colon = TChar(':');
62 +
63 + // Find and skip algorithm prefix if present (e.g., "sha256:")
64 + auto colonPos = id.find(colon);
65 + if (colonPos != std::basic_string_view<TChar>::npos)
66 + {
67 + id.remove_prefix(colonPos + 1);
68 + }
69 +
70 + if (shortenLength && id.length() > shortLen)
71 + {
72 + return std::basic_string<TChar>{id.substr(0, shortLen)};
73 + }
74 +
75 + return std::basic_string<TChar>{id};
76 +}
77 +
78 struct PhysicalMacAddress
79 {
80 BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH]{};
src/windows/common/svccomm.cpp
+24 -374
@@ -29,11 +29,6 @@ Abstract:
29
30 #define IS_VALID_HANDLE(_handle) ((_handle != NULL) && (_handle != INVALID_HANDLE_VALUE))
31
32 -#define TTY_ALT_NUMPAD_VK_MENU (0x12)
33 -#define TTY_ESCAPE_CHARACTER (L'\x1b')
34 -#define TTY_INPUT_EVENT_BUFFER_SIZE (16)
35 -#define TTY_UTF8_TRANSLATION_BUFFER_SIZE (4 * TTY_INPUT_EVENT_BUFFER_SIZE)
36 -
32 using wsl::windows::common::ClientExecutionContext;
33 namespace {
34
@@ -112,64 +107,6 @@ struct CreateProcessArguments
107 std::wstring NtPath{};
108 };
109
115 -BOOL GetNextCharacter(_In_ INPUT_RECORD* InputRecord, _Out_ PWCHAR NextCharacter)
116 -{
117 - BOOL IsNextCharacterValid = FALSE;
118 - if (InputRecord->EventType == KEY_EVENT)
119 - {
120 - const auto KeyEvent = &InputRecord->Event.KeyEvent;
121 - if ((IsActionableKey(KeyEvent) != FALSE) && ((KeyEvent->bKeyDown != FALSE) || (KeyEvent->wVirtualKeyCode == TTY_ALT_NUMPAD_VK_MENU)))
122 - {
123 - *NextCharacter = KeyEvent->uChar.UnicodeChar;
124 - IsNextCharacterValid = TRUE;
125 - }
126 - }
127 -
128 - return IsNextCharacterValid;
129 -}
130 -
131 -BOOL IsActionableKey(_In_ PKEY_EVENT_RECORD KeyEvent)
132 -{
133 - //
134 - // This is a bit complicated to discern.
135 - //
136 - // 1. Our first check is that we only want structures that
137 - // represent at least one key press. If we have 0, then we don't
138 - // need to bother. If we have >1, we'll send the key through
139 - // that many times into the pipe.
140 - // 2. Our second check is where it gets confusing.
141 - // a. Characters that are non-null get an automatic pass. Copy
142 - // them through to the pipe.
143 - // b. Null characters need further scrutiny. We generally do not
144 - // pass nulls through EXCEPT if they're sourced from the
145 - // virtual terminal engine (or another application living
146 - // above our layer). If they're sourced by a non-keyboard
147 - // source, they'll have no scan code (since they didn't come
148 - // from a keyboard). But that rule has an exception too:
149 - // "Enhanced keys" from above the standard range of scan
150 - // codes will return 0 also with a special flag set that says
151 - // they're an enhanced key. That means the desired behavior
152 - // is:
153 - // Scan Code = 0, ENHANCED_KEY = 0
154 - // -> This came from the VT engine or another app
155 - // above our layer.
156 - // Scan Code = 0, ENHANCED_KEY = 1
157 - // -> This came from the keyboard, but is a special
158 - // key like 'Volume Up' that wasn't generally a
159 - // part of historic (pre-1990s) keyboards.
160 - // Scan Code = <anything else>
161 - // -> This came from a keyboard directly.
162 - //
163 -
164 - if ((KeyEvent->wRepeatCount == 0) || ((KeyEvent->uChar.UnicodeChar == UNICODE_NULL) &&
165 - ((KeyEvent->wVirtualScanCode != 0) || (WI_IsFlagSet(KeyEvent->dwControlKeyState, ENHANCED_KEY)))))
166 - {
167 - return FALSE;
168 - }
169 -
170 - return TRUE;
171 -}
172 -
110 void InitializeInterop(_In_ HANDLE ServerPort, _In_ const GUID& DistroId)
111 {
112 //
@@ -211,316 +148,6 @@ void SpawnWslHost(_In_ HANDLE ServerPort, _In_ const GUID& DistroId, _In_opt_ LP
148 // Exported function definitions.
149 //
150
214 -void wsl::windows::common::RelayStandardInput(
215 - HANDLE ConsoleHandle,
216 - HANDLE OutputHandle,
217 - const std::shared_ptr<wsl::shared::SocketChannel>& ControlChannel,
218 - HANDLE ExitEvent,
219 - wsl::windows::common::ConsoleState* Io)
220 -try
221 -{
222 - if (GetFileType(ConsoleHandle) != FILE_TYPE_CHAR)
223 - {
224 - wsl::windows::common::relay::InterruptableRelay(ConsoleHandle, OutputHandle, ExitEvent);
225 - return;
226 - }
227 -
228 - //
229 - // N.B. ReadConsoleInputEx has no associated import library.
230 - //
231 -
232 - static LxssDynamicFunction<decltype(ReadConsoleInputExW)> readConsoleInput(L"Kernel32.dll", "ReadConsoleInputExW");
233 -
234 - INPUT_RECORD InputRecordBuffer[TTY_INPUT_EVENT_BUFFER_SIZE];
235 - INPUT_RECORD* InputRecordPeek = &(InputRecordBuffer[1]);
236 - KEY_EVENT_RECORD* KeyEvent;
237 - DWORD RecordsRead;
238 - OVERLAPPED Overlapped = {0};
239 - const wil::unique_event OverlappedEvent(wil::EventOptions::ManualReset);
240 - Overlapped.hEvent = OverlappedEvent.get();
241 - const HANDLE WaitHandles[] = {ExitEvent, ConsoleHandle};
242 - const std::vector<HANDLE> ExitHandles = {ExitEvent};
243 - for (;;)
244 - {
245 - //
246 - // Because some input events generated by the console are encoded with
247 - // more than one input event, we have to be smart about reading the
248 - // events.
249 - //
250 - // First, we peek at the next input event.
251 - // If it's an escape (wch == L'\x1b') event, then the characters that
252 - // follow are part of an input sequence. We can't know for sure
253 - // how long that sequence is, but we can assume it's all sent to
254 - // the input queue at once, and it's less that 16 events.
255 - // Furthermore, we can assume that if there's an Escape in those
256 - // 16 events, that the escape marks the start of a new sequence.
257 - // So, we'll peek at another 15 events looking for escapes.
258 - // If we see an escape, then we'll read one less than that,
259 - // such that the escape remains the next event in the input.
260 - // From those read events, we'll aggregate chars into a single
261 - // string to send to the subsystem.
262 - // If it's not an escape, send the event through one at a time.
263 - //
264 -
265 - //
266 - // Read one input event.
267 - //
268 -
269 - DWORD WaitStatus = (WAIT_OBJECT_0 + 1);
270 - do
271 - {
272 - THROW_IF_WIN32_BOOL_FALSE(readConsoleInput(ConsoleHandle, InputRecordBuffer, 1, &RecordsRead, CONSOLE_READ_NOWAIT));
273 -
274 - if (RecordsRead == 0)
275 - {
276 - WaitStatus = WaitForMultipleObjects(RTL_NUMBER_OF(WaitHandles), WaitHandles, false, INFINITE);
277 - }
278 - } while ((WaitStatus == (WAIT_OBJECT_0 + 1)) && (RecordsRead == 0));
279 -
280 - //
281 - // Stop processing if the exit event has been signaled.
282 - //
283 -
284 - if (WaitStatus != (WAIT_OBJECT_0 + 1))
285 - {
286 - WI_ASSERT(WaitStatus == WAIT_OBJECT_0);
287 -
288 - break;
289 - }
290 -
291 - WI_ASSERT(RecordsRead == 1);
292 -
293 - //
294 - // Don't read additional records if the first entry is a window size
295 - // event, or a repeated character. Handle those events on their own.
296 - //
297 -
298 - DWORD RecordsPeeked = 0;
299 - if ((InputRecordBuffer[0].EventType != WINDOW_BUFFER_SIZE_EVENT) &&
300 - ((InputRecordBuffer[0].EventType != KEY_EVENT) || (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount < 2)))
301 - {
302 - //
303 - // Read additional input records into the buffer if available.
304 - //
305 -
306 - THROW_IF_WIN32_BOOL_FALSE(PeekConsoleInputW(ConsoleHandle, InputRecordPeek, (RTL_NUMBER_OF(InputRecordBuffer) - 1), &RecordsPeeked));
307 - }
308 -
309 - //
310 - // Iterate over peeked records [1, RecordsPeeked].
311 - //
312 -
313 - DWORD AdditionalRecordsToRead = 0;
314 - WCHAR NextCharacter;
315 - for (DWORD RecordIndex = 1; RecordIndex <= RecordsPeeked; RecordIndex++)
316 - {
317 - if (GetNextCharacter(&InputRecordBuffer[RecordIndex], &NextCharacter) != FALSE)
318 - {
319 - KeyEvent = &InputRecordBuffer[RecordIndex].Event.KeyEvent;
320 - if (NextCharacter == TTY_ESCAPE_CHARACTER)
321 - {
322 - //
323 - // CurrentRecord is an escape event. We will start here
324 - // on the next input loop.
325 - //
326 -
327 - break;
328 - }
329 - else if (KeyEvent->wRepeatCount > 1)
330 - {
331 - //
332 - // Repeated keys are handled on their own. Start with this
333 - // key on the next input loop.
334 - //
335 -
336 - break;
337 - }
338 - else if (IS_HIGH_SURROGATE(NextCharacter) && (RecordIndex >= (RecordsPeeked - 1)))
339 - {
340 - //
341 - // If there is not enough room for the second character of
342 - // a surrogate pair, start with this character on the next
343 - // input loop.
344 - //
345 - // N.B. The test is for at least two remaining records
346 - // because typically a surrogate pair will be entered
347 - // via copy/paste, which will appear as an input
348 - // record with alt-down, alt-up and character. So to
349 - // include the next character of the surrogate pair it
350 - // is likely that the alt-up record will need to be
351 - // read first.
352 - //
353 -
354 - break;
355 - }
356 - }
357 - else if (InputRecordBuffer[RecordIndex].EventType == WINDOW_BUFFER_SIZE_EVENT)
358 - {
359 - //
360 - // A window size event is handled on its own.
361 - //
362 -
363 - break;
364 - }
365 -
366 - //
367 - // Process the additional input record.
368 - //
369 -
370 - AdditionalRecordsToRead += 1;
371 - }
372 -
373 - if (AdditionalRecordsToRead > 0)
374 - {
375 - THROW_IF_WIN32_BOOL_FALSE(readConsoleInput(ConsoleHandle, InputRecordPeek, AdditionalRecordsToRead, &RecordsRead, CONSOLE_READ_NOWAIT));
376 -
377 - if (RecordsRead == 0)
378 - {
379 - //
380 - // This would be an unexpected case. We've already peeked to see
381 - // that there are AdditionalRecordsToRead # of records in the
382 - // input that need reading, yet we didn't get them when we read.
383 - // In this case, move along and finish this input event.
384 - //
385 -
386 - break;
387 - }
388 -
389 - //
390 - // We already had one input record in the buffer before reading
391 - // additional, So account for that one too
392 - //
393 -
394 - RecordsRead += 1;
395 - }
396 -
397 - //
398 - // Process each input event. Keydowns will get aggregated into
399 - // Utf8String before getting injected into the subsystem.
400 - //
401 -
402 - WCHAR Utf16String[TTY_INPUT_EVENT_BUFFER_SIZE];
403 - ULONG Utf16StringSize = 0;
404 - COORD WindowSize{};
405 - LX_INIT_WINDOW_SIZE_CHANGED WindowSizeMessage{};
406 - for (DWORD RecordIndex = 0; RecordIndex < RecordsRead; RecordIndex++)
407 - {
408 - INPUT_RECORD* CurrentInputRecord = &(InputRecordBuffer[RecordIndex]);
409 - switch (CurrentInputRecord->EventType)
410 - {
411 - case KEY_EVENT:
412 -
413 - //
414 - // Filter out key up events unless they are from an <Alt> key.
415 - // Key up with an <Alt> key could contain a Unicode character
416 - // pasted from the clipboard and converted to an <Alt>+<Numpad> sequence.
417 - //
418 -
419 - KeyEvent = &CurrentInputRecord->Event.KeyEvent;
420 - if ((KeyEvent->bKeyDown == FALSE) && (KeyEvent->wVirtualKeyCode != TTY_ALT_NUMPAD_VK_MENU))
421 - {
422 - break;
423 - }
424 -
425 - //
426 - // Filter out key presses that are not actionable, such as just
427 - // pressing <Ctrl>, <Alt>, <Shift> etc. These key presses return
428 - // the character of null but will have a valid scan code off the
429 - // keyboard. Certain other key sequences such as Ctrl+A,
430 - // Ctrl+<space>, and Ctrl+@ will also return the character null
431 - // but have no scan code.
432 - // <Alt> + <NumPad> sequences will show an <Alt> but will have
433 - // a scancode and character specified, so they should be actionable.
434 - //
435 -
436 - if (IsActionableKey(KeyEvent) == FALSE)
437 - {
438 - break;
439 - }
440 -
441 - Utf16String[Utf16StringSize] = KeyEvent->uChar.UnicodeChar;
442 - Utf16StringSize += 1;
443 - break;
444 -
445 - case WINDOW_BUFFER_SIZE_EVENT:
446 -
447 - //
448 - // Query the window size and send an update message via the
449 - // control channel.
450 - //
451 - if (ControlChannel)
452 - {
453 - WindowSize = Io->GetWindowSize();
454 - WindowSizeMessage.Header.MessageType = LxInitMessageWindowSizeChanged;
455 - WindowSizeMessage.Header.MessageSize = sizeof(WindowSizeMessage);
456 - WindowSizeMessage.Columns = WindowSize.X;
457 - WindowSizeMessage.Rows = WindowSize.Y;
458 -
459 - try
460 - {
461 - ControlChannel->SendMessage(WindowSizeMessage);
462 - }
463 - CATCH_LOG();
464 - }
465 -
466 - break;
467 - }
468 - }
469 -
470 - CHAR Utf8String[TTY_UTF8_TRANSLATION_BUFFER_SIZE];
471 - DWORD Utf8StringSize = 0;
472 - if (Utf16StringSize > 0)
473 - {
474 - //
475 - // Windows uses UTF-16LE encoding, Linux uses UTF-8 by default.
476 - // Convert each UTF-16LE character into the proper UTF-8 byte
477 - // sequence equivalent.
478 - //
479 -
480 - THROW_LAST_ERROR_IF(
481 - (Utf8StringSize = WideCharToMultiByte(
482 - CP_UTF8, 0, Utf16String, Utf16StringSize, Utf8String, sizeof(Utf8String), nullptr, nullptr)) == 0);
483 - }
484 -
485 - //
486 - // Send the input bytes to the terminal.
487 - //
488 -
489 - DWORD BytesWritten = 0;
490 - const auto Utf8Span = gslhelpers::struct_as_bytes(Utf8String).first(Utf8StringSize);
491 - if ((RecordsRead == 1) && (InputRecordBuffer[0].EventType == KEY_EVENT) && (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount > 1))
492 - {
493 - WI_ASSERT(Utf16StringSize == 1);
494 -
495 - //
496 - // Handle repeated characters. They aren't part of an input
497 - // sequence, so there's only one event that's generating characters.
498 - //
499 -
500 - WORD RepeatIndex;
501 - for (RepeatIndex = 0; RepeatIndex < InputRecordBuffer[0].Event.KeyEvent.wRepeatCount; RepeatIndex += 1)
502 - {
503 - BytesWritten = wsl::windows::common::relay::InterruptableWrite(OutputHandle, Utf8Span, ExitHandles, &Overlapped);
504 - if (BytesWritten == 0)
505 - {
506 - break;
507 - }
508 - }
509 - }
510 - else if (Utf8StringSize > 0)
511 - {
512 - BytesWritten = wsl::windows::common::relay::InterruptableWrite(OutputHandle, Utf8Span, ExitHandles, &Overlapped);
513 - if (BytesWritten == 0)
514 - {
515 - break;
516 - }
517 - }
518 - }
519 -
520 - return;
521 -}
522 -CATCH_LOG()
523 -
151 wsl::windows::common::SvcComm::SvcComm()
152 {
153 // Ensure that the OS has support for running lifted WSL. This interface is always present on Windows 11 and later.
@@ -820,7 +447,30 @@ wsl::windows::common::SvcComm::LaunchProcess(
447 if (IS_VALID_HANDLE(StdIn))
448 {
449 std::thread([StdIn, StdInSocket = std::move(StdInSocket), ControlChannel = ControlChannel, ExitHandle = ExitEvent.get(), Io = &Io]() mutable {
823 - RelayStandardInput(StdIn, StdInSocket.get(), ControlChannel, ExitHandle, Io);
450 + auto updateTerminal = [&]() {
451 + //
452 + // Query the window size and send an update message via the
453 + // control channel.
454 + //
455 + if (ControlChannel)
456 + {
457 + auto WindowSize = Io->GetWindowSize();
458 +
459 + LX_INIT_WINDOW_SIZE_CHANGED WindowSizeMessage{};
460 + WindowSizeMessage.Header.MessageType = LxInitMessageWindowSizeChanged;
461 + WindowSizeMessage.Header.MessageSize = sizeof(WindowSizeMessage);
462 + WindowSizeMessage.Columns = WindowSize.X;
463 + WindowSizeMessage.Rows = WindowSize.Y;
464 +
465 + try
466 + {
467 + ControlChannel->SendMessage(WindowSizeMessage);
468 + }
469 + CATCH_LOG();
470 + }
471 + };
472 +
473 + wsl::windows::common::relay::StandardInputRelay(StdIn, StdInSocket.get(), updateTerminal, ExitHandle);
474 }).detach();
475 }
476
src/windows/common/svccomm.hpp
-2
@@ -21,8 +21,6 @@ Abstract:
21
22 namespace wsl::windows::common {
23
24 -void RelayStandardInput(HANDLE ConsoleHandle, HANDLE OutputHandle, const std::shared_ptr<wsl::shared::SocketChannel>& ControlChannel, HANDLE ExitEvent, ConsoleState* Io);
25 -
24 class SvcComm
25 {
26 public:
src/windows/common/wslutil.cpp
+353 -22
@@ -15,7 +15,9 @@ Abstract:
15 #include "precomp.h"
16 #include "wslutil.h"
17 #include "WslPluginApi.h"
18 +#include <wincrypt.h>
19 #include "wslinstallerservice.h"
20 +#include "wslc.h"
21
22 #include "ConsoleProgressBar.h"
23 #include "ExecutionContext.h"
@@ -86,6 +88,7 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
88 X(WSL_E_INVALID_JSON),
89 X(WSL_E_VM_CRASHED),
90 X(WSL_E_NOT_A_LINUX_DISTRO),
91 + X(WSLC_E_CONTAINER_PREFIX_AMBIGUOUS),
92 X(E_ACCESSDENIED),
93 X_WIN32(ERROR_NOT_FOUND),
94 X_WIN32(ERROR_VERSION_PARSE_ERROR),
@@ -135,8 +138,28 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
138 X_WIN32(ERROR_INVALID_SECURITY_DESCR),
139 X(VM_E_INVALID_STATE),
140 X_WIN32(STATUS_SHUTDOWN_IN_PROGRESS),
141 + X(WININET_E_TIMEOUT),
142 + X(WSAEADDRNOTAVAIL),
143 + X_WIN32(ERROR_BAD_IMPERSONATION_LEVEL),
144 + X_WIN32(ERROR_NO_DATA),
145 + X_WIN32(WSAETIMEDOUT),
146 + X_WIN32(ERROR_OPERATION_ABORTED),
147 + X_WIN32(WSAECONNREFUSED),
148 X_WIN32(ERROR_BAD_PATHNAME),
139 - X(WININET_E_TIMEOUT)};
149 + X(WININET_E_TIMEOUT),
150 + X_WIN32(ERROR_INVALID_SID),
151 + X_WIN32(ERROR_INVALID_STATE),
152 + X(WSLC_E_IMAGE_NOT_FOUND),
153 + X(WSLC_E_CONTAINER_NOT_FOUND),
154 + X(WSLC_E_VOLUME_NOT_FOUND),
155 + X(WSLC_E_CONTAINER_NOT_RUNNING),
156 + X(WSLC_E_CONTAINER_IS_RUNNING),
157 + X(WSLC_E_SESSION_RESERVED),
158 + X(WSLC_E_INVALID_SESSION_NAME),
159 + X(WSLC_E_NETWORK_NOT_FOUND),
160 + X(WSLC_E_WU_SEARCH_FAILED),
161 + X_WIN32(RPC_S_SERVER_UNAVAILABLE),
162 + X_WIN32(ERROR_ELEVATION_REQUIRED)};
163
164 #undef X
165
@@ -182,7 +205,8 @@ static const std::map<Context, LPCWSTR> g_contextStrings{
205 X(HNS),
206 X(ReadDistroConfig),
207 X(MoveDistro),
185 - X(VerifyChecksum)};
208 + X(VerifyChecksum),
209 + X(WslC)};
210
211 #undef X
212
@@ -239,6 +263,28 @@ constexpr GUID EndianSwap(GUID value)
263 return value;
264 }
265
266 +std::regex BuildImageReferenceRegex()
267 +{
268 + // See: https://github.com/containers/image/blob/main/docker/reference/regexp.go
269 +
270 + std::string alphaNum = "[a-z0-9]+";
271 + std::string separator = "(?:[._]|__|[-]*)";
272 + std::string domainComponent = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
273 + std::string tag = "[\\w][\\w.-]{0,127}";
274 + std::string digest = "[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}";
275 +
276 + auto group = [](const auto& exp) { return std::format("(?:{})", exp); };
277 + auto optional = [&group](const auto& exp) { return group(exp) + "?"; };
278 + auto repeated = [&group](const auto& exp) { return group(exp) + "+"; };
279 + auto capture = [](const auto& exp) { return std::format("({})", exp); };
280 +
281 + auto nameComponent = alphaNum + optional(repeated(separator + alphaNum));
282 + auto domain = domainComponent + optional(repeated("\\." + domainComponent)) + optional(":[0-9]+");
283 + auto namePat = optional(domain + "\\/") + nameComponent + optional(repeated("\\/" + nameComponent));
284 +
285 + return std::regex("^" + capture(namePat) + optional(":" + capture(tag)) + optional("@" + capture(digest)) + "$");
286 +}
287 +
288 } // namespace
289
290 template <typename TInterface>
@@ -297,6 +343,20 @@ GUID wsl::windows::common::wslutil::CreateV5Uuid(const GUID& namespaceGuid, cons
343 }
344
345 std::wstring wsl::windows::common::wslutil::DownloadFile(std::wstring_view Url, std::wstring Filename)
346 +{
347 + wsl::windows::common::ConsoleProgressBar progressBar;
348 + auto progress = [&](auto current, auto total) {
349 + progressBar.Print(current, total);
350 + return true;
351 + };
352 +
353 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { progressBar.Clear(); });
354 +
355 + return DownloadFileImpl(Url, Filename, progress);
356 +}
357 +
358 +std::wstring wsl::windows::common::wslutil::DownloadFileImpl(
359 + std::wstring_view Url, std::wstring Filename, const std::function<void(uint64_t, uint64_t)>& Progress)
360 {
361 const auto lastSlash = Url.find_last_of('/');
362 THROW_HR_IF(E_INVALIDARG, lastSlash == std::wstring::npos);
@@ -326,7 +386,6 @@ std::wstring wsl::windows::common::wslutil::DownloadFile(std::wstring_view Url,
386 const auto asyncResponse = client.GetInputStreamAsync(winrt::Windows::Foundation::Uri(Url));
387
388 std::atomic<uint64_t> totalBytes;
329 - wsl::windows::common::ConsoleProgressBar progressBar;
389 asyncResponse.Progress(
390 [&](const winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Windows::Storage::Streams::IInputStream, winrt::Windows::Web::Http::HttpProgress>&,
391 const winrt::Windows::Web::Http::HttpProgress& progress) {
@@ -341,12 +400,11 @@ std::wstring wsl::windows::common::wslutil::DownloadFile(std::wstring_view Url,
400 download.Progress([&](const auto& _, uint64_t progress) {
401 if (totalBytes != 0)
402 {
344 - progressBar.Print(progress, totalBytes);
403 + Progress(progress, totalBytes);
404 }
405 });
406
407 download.get();
349 - progressBar.Clear();
408 deleteFileOnFailure.release();
409
410 return file.Path().c_str();
@@ -361,13 +419,14 @@ std::wstring wsl::windows::common::wslutil::DownloadFile(std::wstring_view Url,
419 return newHandle;
420 }
421
364 -[[nodiscard]] HANDLE wsl::windows::common::wslutil::DuplicateHandleFromCallingProcess(_In_ HANDLE Handle)
422 +[[nodiscard]] HANDLE wsl::windows::common::wslutil::DuplicateHandleFromCallingProcess(_In_ HANDLE Handle, _In_ std::optional<DWORD> DesiredAccess)
423 {
424 const wil::unique_handle caller = OpenCallingProcess(PROCESS_DUP_HANDLE);
425 THROW_LAST_ERROR_IF(!caller);
426
427 HANDLE newHandle;
370 - THROW_IF_WIN32_BOOL_FALSE(::DuplicateHandle(caller.get(), Handle, GetCurrentProcess(), &newHandle, 0, FALSE, DUPLICATE_SAME_ACCESS));
428 + THROW_IF_WIN32_BOOL_FALSE(::DuplicateHandle(
429 + caller.get(), Handle, GetCurrentProcess(), &newHandle, DesiredAccess.value_or(0), FALSE, DesiredAccess.has_value() ? 0 : DUPLICATE_SAME_ACCESS));
430
431 return newHandle;
432 }
@@ -431,7 +490,7 @@ std::wstring wsl::windows::common::wslutil::ErrorCodeToString(HRESULT Error)
490
491 wsl::windows::common::ErrorStrings wsl::windows::common::wslutil::ErrorToString(const Error& error)
492 {
434 - ErrorStrings errorStrings;
493 + ErrorStrings errorStrings{.Source = error.Source};
494
495 if (error.Message.has_value())
496 {
@@ -478,6 +537,23 @@ wsl::windows::common::ErrorStrings wsl::windows::common::wslutil::ErrorToString(
537 return errorStrings;
538 }
539
540 +[[nodiscard]] HANDLE wsl::windows::common::wslutil::FromCOMInputHandle(WSLCHandle Handle)
541 +{
542 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE), Handle.Handle.File == nullptr || Handle.Handle.File == INVALID_HANDLE_VALUE);
543 +
544 + switch (Handle.Type)
545 + {
546 + case WSLCHandleTypeFile:
547 + return Handle.Handle.File;
548 + case WSLCHandleTypePipe:
549 + return Handle.Handle.Pipe;
550 + case WSLCHandleTypeSocket:
551 + return Handle.Handle.Socket;
552 + default:
553 + THROW_HR_MSG(E_UNEXPECTED, "Unsupported handle type: %d", Handle.Type);
554 + }
555 +}
556 +
557 std::wstring wsl::windows::common::wslutil::ConstructPipePath(std::wstring_view PipeName)
558 {
559 return c_pipePrefix + std::wstring(PipeName);
@@ -492,6 +568,24 @@ std::filesystem::path wsl::windows::common::wslutil::GetBasePath()
568 return std::filesystem::path(std::move(path));
569 }
570
571 +std::optional<COMErrorInfo> wsl::windows::common::wslutil::GetCOMErrorInfo()
572 +{
573 + wil::com_ptr<IErrorInfo> errorInfo;
574 + THROW_IF_FAILED(GetErrorInfo(0, &errorInfo));
575 +
576 + if (!errorInfo)
577 + {
578 + return {};
579 + }
580 +
581 + COMErrorInfo error{};
582 +
583 + THROW_IF_FAILED(errorInfo->GetDescription(&error.Message));
584 + THROW_IF_FAILED(errorInfo->GetSource(&error.Source));
585 +
586 + return error;
587 +}
588 +
589 std::wstring wsl::windows::common::wslutil::GetDebugShellPipeName(_In_ PSID Sid)
590 {
591 return ConstructPipePath(std::wstring(L"wsl_debugshell_") + SidToString(Sid).get());
@@ -574,23 +668,13 @@ std::wstring wsl::windows::common::wslutil::GetErrorString(HRESULT result)
668 errorString = Localization::MessageNoDefaultDistro();
669 break;
670
577 - case HRESULT_FROM_WIN32(WSAECONNABORTED):
578 - case HRESULT_FROM_WIN32(ERROR_SHUTDOWN_IN_PROGRESS):
579 - return Localization::MessageInstanceTerminated();
580 -
671 case WSL_E_DISTRO_NOT_FOUND:
672 errorString = Localization::MessageDistroNotFound();
673 break;
674
585 - case HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS):
586 - return Localization::MessageDistroNameAlreadyExists();
587 -
675 case WSL_E_DISTRIBUTION_NAME_NEEDED:
676 return Localization::MessageDistributionNameNeeded();
677
591 - case HRESULT_FROM_WIN32(ERROR_FILE_EXISTS):
592 - return Localization::MessageDistroInstallPathAlreadyExists();
593 -
678 case WSL_E_TOO_MANY_DISKS_ATTACHED:
679 return Localization::MessageTooManyDisks();
680
@@ -973,6 +1057,25 @@ std::vector<BYTE> wsl::windows::common::wslutil::HashFile(HANDLE file, DWORD Alg
1057 return fileHash;
1058 }
1059
1060 +std::optional<std::tuple<uint32_t, uint32_t, uint32_t>> wsl::windows::common::wslutil::GetInstalledPackageVersion()
1061 +{
1062 + std::wstring packageVersion;
1063 + auto result = wil::ResultFromException([&]() {
1064 + auto msiKey = wsl::windows::common::registry::OpenLxssMachineKey(KEY_READ);
1065 +
1066 + packageVersion = wsl::windows::common::registry::ReadString(msiKey.get(), L"Msi", L"Version");
1067 + });
1068 +
1069 + if (result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || result == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND))
1070 + {
1071 + return {};
1072 + }
1073 +
1074 + THROW_IF_FAILED(result);
1075 +
1076 + return ParseWslPackageVersion(packageVersion);
1077 +}
1078 +
1079 void wsl::windows::common::wslutil::InitializeWil()
1080 {
1081 wil::WilInitialize_CppWinRT();
@@ -1033,6 +1136,44 @@ std::vector<DWORD> wsl::windows::common::wslutil::ListRunningProcesses()
1136 return pids;
1137 }
1138
1139 +std::pair<std::string, std::string> wsl::windows::common::wslutil::NormalizeRepo(const std::string& Input)
1140 +{
1141 + // See: https://github.com/distribution/reference/blob/ff14fafe2236e51c2894ac07d4bdfc778e96d682/normalize.go#L126
1142 +
1143 + constexpr auto defaultDomain = "docker.io";
1144 + constexpr auto officialPrefix = "library/";
1145 + constexpr auto legacyDomain = "index.docker.io";
1146 + constexpr auto localhost = "localhost";
1147 +
1148 + auto slash = Input.find('/');
1149 + if (slash == std::string::npos)
1150 + {
1151 + return {defaultDomain, officialPrefix + Input};
1152 + }
1153 +
1154 + auto domain = Input.substr(0, slash);
1155 + auto path = Input.substr(slash + 1);
1156 +
1157 + if (domain == legacyDomain)
1158 + {
1159 + domain = defaultDomain;
1160 + }
1161 + else if (domain != localhost && domain.find_first_of(".:") == std::string::npos && !std::ranges::any_of(domain, [](unsigned char e) {
1162 + return std::isupper(e);
1163 + }))
1164 + {
1165 + domain = defaultDomain;
1166 + path = Input;
1167 + }
1168 +
1169 + if (domain == defaultDomain && path.find('/') == std::string::npos)
1170 + {
1171 + path = "library/" + path;
1172 + }
1173 +
1174 + return {domain, path};
1175 +}
1176 +
1177 std::pair<wil::unique_hfile, wil::unique_hfile> wsl::windows::common::wslutil::OpenAnonymousPipe(DWORD Size, bool ReadPipeOverlapped, bool WritePipeOverlapped)
1178 {
1179 // Default to 4096 byte buffer, just like CreatePipe().
@@ -1090,8 +1231,8 @@ std::pair<wil::unique_hfile, wil::unique_hfile> wsl::windows::common::wslutil::O
1231
1232 bool wsl::windows::common::wslutil::IsVirtualMachinePlatformInstalled()
1233 {
1093 - // Note for Windows 11 22H2 and above builds: If hyper-v is installed but VMP platform isn't, HNS and vmcompute are available
1094 - // but calls to HNS will fail if vfpext isn't installed.
1234 + // Note for Windows 11 22H2 and above builds: If hyper-v is installed but VMP platform isn't, HNS and vmcompute are
1235 + // available but calls to HNS will fail if vfpext isn't installed.
1236 return wsl::windows::common::helpers::IsServicePresent(L"HNS") &&
1237 wsl::windows::common::helpers::IsServicePresent(L"vmcompute") &&
1238 (helpers::GetWindowsVersion().BuildNumber < helpers::WindowsBuildNumbers::Nickel ||
@@ -1110,6 +1251,22 @@ wil::unique_handle wsl::windows::common::wslutil::OpenCallingProcess(_In_ DWORD
1251 return caller;
1252 }
1253
1254 +void wsl::windows::common::wslutil::ParseIpv4Address(const char* Address, in_addr& Result)
1255 +{
1256 + if (inet_pton(AF_INET, Address, &Result) != 1)
1257 + {
1258 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageInvalidIp(Address));
1259 + }
1260 +}
1261 +
1262 +void wsl::windows::common::wslutil::ParseIpv6Address(const char* Address, in_addr6& Result)
1263 +{
1264 + if (inet_pton(AF_INET6, Address, &Result) != 1)
1265 + {
1266 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageInvalidIp(Address));
1267 + }
1268 +}
1269 +
1270 std::tuple<uint32_t, uint32_t, uint32_t> wsl::windows::common::wslutil::ParseWslPackageVersion(_In_ const std::wstring& Version)
1271 {
1272 const std::wregex pattern(L"(\\d+)\\.(\\d+)\\.(\\d+).*");
@@ -1131,6 +1288,42 @@ std::tuple<uint32_t, uint32_t, uint32_t> wsl::windows::common::wslutil::ParseWsl
1288 }
1289 }
1290
1291 +std::pair<std::string, std::optional<std::string>> wsl::windows::common::wslutil::ParseImage(const std::string& Input, EnumReferenceFormat* Format)
1292 +{
1293 + static const auto regex = BuildImageReferenceRegex();
1294 + std::smatch match;
1295 + if (!std::regex_match(Input, match, regex))
1296 + {
1297 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageWslcInvalidImage(Input.c_str()));
1298 + }
1299 +
1300 + const auto& repo = match[1];
1301 + const auto& tag = match[2];
1302 + const auto& digest = match[3];
1303 +
1304 + THROW_HR_IF_MSG(E_UNEXPECTED, !repo.matched, "Unexpected regex match. Input: %hs", Input.c_str());
1305 +
1306 + EnumReferenceFormat referenceFormat = EnumReferenceFormat::None;
1307 + std::optional<std::string> tagOrDigest;
1308 + if (digest.matched) // <repo>:[tag]@<digest> (If both digest and tag are specified, digest takes precedence).
1309 + {
1310 + tagOrDigest = digest.str();
1311 + referenceFormat = EnumReferenceFormat::Digest;
1312 + }
1313 + else if (tag.matched) // <repo>:<tag>
1314 + {
1315 + tagOrDigest = tag.str();
1316 + referenceFormat = EnumReferenceFormat::Tag;
1317 + }
1318 +
1319 + if (Format)
1320 + {
1321 + *Format = referenceFormat;
1322 + }
1323 +
1324 + return {repo.str(), std::move(tagOrDigest)};
1325 +}
1326 +
1327 void wsl::windows::common::wslutil::PrintSystemError(_In_ HRESULT result, _Inout_ FILE* const stream)
1328 {
1329 fwprintf(stream, L"%ls\n", GetSystemErrorString(result).c_str());
@@ -1170,8 +1363,15 @@ void wsl::windows::common::wslutil::SetCrtEncoding(int Mode)
1363 setMode(stdout, Mode);
1364 setMode(stderr, Mode);
1365
1173 - // Set the locale to the current environment's default locale.
1366 + // Set the locale to the current environment's default locale for regional
1367 + // formatting (numeric, time, collation), then override LC_CTYPE to UTF-8
1368 + // so that narrow-to-wide conversions (e.g. %hs in wprintf) correctly decode
1369 + // UTF-8 multi-byte sequences from Linux/container processes.
1370 WI_VERIFY(_wsetlocale(LC_ALL, L"") != NULL);
1371 + if (Mode == _O_U8TEXT)
1372 + {
1373 + WI_VERIFY(_wsetlocale(LC_CTYPE, L".UTF-8") != NULL);
1374 + }
1375 }
1376
1377 void wsl::windows::common::wslutil::SetThreadDescription(LPCWSTR Name)
@@ -1187,6 +1387,70 @@ wil::unique_hlocal_string wsl::windows::common::wslutil::SidToString(_In_ PSID U
1387 return sid;
1388 }
1389
1390 +WSLCHandle wsl::windows::common::wslutil::ToCOMOutputHandle(HANDLE Handle, DWORD Access)
1391 +{
1392 + wil::unique_handle duplicatedHandle{DuplicateHandle(Handle, Access)};
1393 +
1394 + // N.B. COM closes the handle when returning an out parameter.
1395 + // The return value of this method should always be passed to a COM out parameter.
1396 + auto comHandle = ToCOMInputHandle(duplicatedHandle.release());
1397 +
1398 + return comHandle;
1399 +}
1400 +
1401 +WSLCHandle wsl::windows::common::wslutil::ToCOMOutputHandle(HANDLE Handle, DWORD Access, WSLCHandleType Type)
1402 +{
1403 + wil::unique_handle duplicatedHandle{DuplicateHandle(Handle, Access)};
1404 +
1405 + // N.B. COM closes the handle when returning an out parameter.
1406 + // The return value of this method should always be passed to a COM out parameter.
1407 + switch (Type)
1408 + {
1409 + case WSLCHandleTypeFile:
1410 + return WSLCHandle{.Type = WSLCHandleTypeFile, .Handle = {.File = duplicatedHandle.release()}};
1411 + case WSLCHandleTypePipe:
1412 + return WSLCHandle{.Type = WSLCHandleTypePipe, .Handle = {.Pipe = duplicatedHandle.release()}};
1413 + case WSLCHandleTypeSocket:
1414 + return WSLCHandle{.Type = WSLCHandleTypeSocket, .Handle = {.Socket = duplicatedHandle.release()}};
1415 + default:
1416 + THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported handle type: %d", static_cast<int>(Type));
1417 + }
1418 +}
1419 +
1420 +WSLCHandle wsl::windows::common::wslutil::ToCOMInputHandle(HANDLE Handle)
1421 +{
1422 + auto type = GetFileType(Handle);
1423 + if (type == FILE_TYPE_PIPE)
1424 + {
1425 + int socketType{};
1426 + int len = sizeof(socketType);
1427 +
1428 + // N.B. FILE_TYPE_PIPE can describe a pipe, a named pipe, or a socket.
1429 + // Check for a named pipe first, since getsockopt() can return success for a named pipe.
1430 +
1431 + if (GetNamedPipeInfo(Handle, nullptr, nullptr, nullptr, nullptr))
1432 + {
1433 + return WSLCHandle{.Type = WSLCHandleTypePipe, .Handle = {.Pipe = Handle}};
1434 + }
1435 + else if (getsockopt(reinterpret_cast<SOCKET>(Handle), SOL_SOCKET, SO_TYPE, reinterpret_cast<char*>(&socketType), &len) == 0)
1436 + {
1437 + return WSLCHandle{.Type = WSLCHandleTypeSocket, .Handle = {.Socket = Handle}};
1438 + }
1439 + else
1440 + {
1441 + return WSLCHandle{.Type = WSLCHandleTypePipe, .Handle = {.Pipe = Handle}};
1442 + }
1443 + }
1444 + else if (type == FILE_TYPE_DISK)
1445 + {
1446 + return WSLCHandle{.Type = WSLCHandleTypeFile, .Handle = {.File = Handle}};
1447 + }
1448 + else
1449 + {
1450 + THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported handle type: %d", type);
1451 + }
1452 +}
1453 +
1454 winrt::Windows::Management::Deployment::PackageVolume wsl::windows::common::wslutil::GetSystemVolume()
1455 try
1456 {
@@ -1207,4 +1471,71 @@ catch (...)
1471 {
1472 LOG_CAUGHT_EXCEPTION();
1473 return nullptr;
1210 -}
\ No newline at end of file
1474 +}
1475 +
1476 +std::string wsl::windows::common::wslutil::Base64Encode(const std::string& input)
1477 +{
1478 + DWORD base64Size = 0;
1479 + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA(
1480 + reinterpret_cast<const BYTE*>(input.c_str()), static_cast<DWORD>(input.size()), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &base64Size));
1481 +
1482 + auto buffer = std::make_unique<char[]>(base64Size);
1483 + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA(
1484 + reinterpret_cast<const BYTE*>(input.c_str()),
1485 + static_cast<DWORD>(input.size()),
1486 + CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,
1487 + buffer.get(),
1488 + &base64Size));
1489 +
1490 + return std::string(buffer.get());
1491 +}
1492 +
1493 +std::string wsl::windows::common::wslutil::Base64Decode(const std::string& encoded)
1494 +{
1495 + DWORD size = 0;
1496 + THROW_IF_WIN32_BOOL_FALSE(CryptStringToBinaryA(
1497 + encoded.c_str(), static_cast<DWORD>(encoded.size()), CRYPT_STRING_BASE64, nullptr, &size, nullptr, nullptr));
1498 +
1499 + std::string result(size, '\0');
1500 + THROW_IF_WIN32_BOOL_FALSE(CryptStringToBinaryA(
1501 + encoded.c_str(), static_cast<DWORD>(encoded.size()), CRYPT_STRING_BASE64, reinterpret_cast<BYTE*>(result.data()), &size, nullptr, nullptr));
1502 +
1503 + result.resize(size);
1504 + return result;
1505 +}
1506 +
1507 +std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& username, const std::string& password)
1508 +{
1509 + nlohmann::json authJson = {{"username", username}, {"password", password}};
1510 + return Base64Encode(authJson.dump());
1511 +}
1512 +
1513 +std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& identityToken)
1514 +{
1515 + nlohmann::json authJson = {{"identitytoken", identityToken}};
1516 + return Base64Encode(authJson.dump());
1517 +}
1518 +
1519 +std::map<std::string, std::string> wsl::windows::common::wslutil::ParseKeyValuePairs(const KeyValuePair* pairs, ULONG count, LPCSTR reservedKey)
1520 +{
1521 + THROW_HR_IF(E_POINTER, count > 0 && pairs == nullptr);
1522 +
1523 + std::map<std::string, std::string> result;
1524 +
1525 + for (ULONG i = 0; i < count; i++)
1526 + {
1527 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, pairs[i].Key, "Key at index %lu is null", i);
1528 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, pairs[i].Value, "Value at index %lu is null", i);
1529 +
1530 + if (reservedKey != nullptr)
1531 + {
1532 + THROW_HR_IF_MSG(E_INVALIDARG, strcmp(pairs[i].Key, reservedKey) == 0, "Key '%hs' is reserved", reservedKey);
1533 + }
1534 +
1535 + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), result.contains(pairs[i].Key), "Duplicate key: '%hs'", pairs[i].Key);
1536 +
1537 + result[pairs[i].Key] = pairs[i].Value;
1538 + }
1539 +
1540 + return result;
1541 +}
src/windows/common/wslutil.h
+148 -3
@@ -19,6 +19,7 @@ Abstract:
19 #include "SubProcess.h"
20 #include <winrt/windows.management.deployment.h>
21 #include "JsonUtils.h"
22 +#include "wslc.h"
23
24 namespace wsl::windows::common {
25 struct Error;
@@ -27,6 +28,7 @@ struct ErrorStrings
28 {
29 std::wstring Message;
30 std::wstring Code;
31 + std::optional<std::wstring> Source;
32 };
33 } // namespace wsl::windows::common
34
@@ -44,7 +46,14 @@ inline auto c_msixPackageFamilyName = L"MicrosoftCorporationII.WindowsSubsystemF
46 inline auto c_githubUrlOverrideRegistryValue = L"GitHubUrlOverride";
47 inline auto c_vhdFileExtension = L".vhd";
48 inline auto c_vhdxFileExtension = L".vhdx";
47 -inline constexpr auto c_vmOwner = L"WSL";
49 +inline constexpr auto c_vmOwner = L"WSL"; // TODO-WSLC: Does this apply to WSLC ?
50 +
51 +enum class EnumReferenceFormat
52 +{
53 + None,
54 + Tag,
55 + Digest
56 +};
57
58 struct GitHubReleaseAsset
59 {
@@ -64,6 +73,109 @@ struct GitHubRelease
73 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GitHubRelease, name, assets, created_at);
74 };
75
76 +struct COMErrorInfo
77 +{
78 + wil::unique_bstr Message;
79 + wil::unique_bstr Source;
80 +};
81 +
82 +static_assert(sizeof(WSLCHandle::Handle) == sizeof(HANDLE));
83 +static_assert(sizeof(FILE_HANDLE) == sizeof(HANDLE));
84 +static_assert(sizeof(PIPE_HANDLE) == sizeof(HANDLE));
85 +static_assert(sizeof(SOCKET_HANDLE) == sizeof(HANDLE));
86 +
87 +struct COMOutputHandle : public WSLCHandle
88 +{
89 + NON_COPYABLE(COMOutputHandle);
90 + NON_MOVABLE(COMOutputHandle);
91 + COMOutputHandle()
92 + {
93 + ZeroMemory(&Handle, sizeof(Handle));
94 + Type = WSLCHandleTypeUnknown;
95 + }
96 +
97 + ~COMOutputHandle()
98 + {
99 + Reset();
100 + }
101 +
102 + void Reset() noexcept
103 + {
104 + if (!Empty())
105 + {
106 + LOG_IF_WIN32_BOOL_FALSE(CloseHandle(Handle.File));
107 + Handle.File = nullptr;
108 + }
109 + }
110 +
111 + [[nodiscard]] wil::unique_handle Release() noexcept
112 + {
113 + wil::unique_handle handle(Handle.File);
114 + Handle.File = nullptr;
115 +
116 + return handle;
117 + }
118 +
119 + HANDLE Get() const noexcept
120 + {
121 + return Handle.File;
122 + }
123 +
124 + bool Empty() const noexcept
125 + {
126 + return Handle.File == nullptr || Handle.File == INVALID_HANDLE_VALUE;
127 + }
128 +};
129 +
130 +struct PruneResult
131 +{
132 + NON_COPYABLE(PruneResult);
133 + WSLCPruneContainersResults result{};
134 +
135 + PruneResult() = default;
136 +
137 + PruneResult(PruneResult&& other)
138 + {
139 + *this = std::move(other);
140 + }
141 +
142 + PruneResult& operator=(PruneResult&& other)
143 + {
144 + CoTaskMemFree(result.Containers);
145 + result.Containers = other.result.Containers;
146 + result.ContainersCount = other.result.ContainersCount;
147 + result.SpaceReclaimed = other.result.SpaceReclaimed;
148 +
149 + other.result.Containers = nullptr;
150 + other.result.ContainersCount = 0;
151 + other.result.SpaceReclaimed = 0;
152 +
153 + return *this;
154 + }
155 +
156 + ~PruneResult()
157 + {
158 + CoTaskMemFree(result.Containers);
159 + }
160 +};
161 +
162 +class StopWatch
163 +{
164 + NON_COPYABLE(StopWatch);
165 + NON_MOVABLE(StopWatch);
166 +
167 +public:
168 + StopWatch() = default;
169 +
170 + uint64_t ElapsedMilliseconds() const
171 + {
172 + return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_startTime).count();
173 + }
174 +
175 +private:
176 + std::chrono::steady_clock::time_point m_startTime = std::chrono::steady_clock::now();
177 +};
178 +
179 template <typename T>
180 void AssertValidPrintfArg()
181 {
@@ -99,11 +211,13 @@ GUID CreateV5Uuid(const GUID& namespaceGuid, const std::span<const std::byte> na
211
212 std::wstring DownloadFile(std::wstring_view Url, std::wstring Filename);
213
214 +std::wstring DownloadFileImpl(std::wstring_view Url, std::wstring Filename, const std::function<void(uint64_t, uint64_t)>& Progress);
215 +
216 [[nodiscard]] HANDLE DuplicateHandle(_In_ HANDLE Handle, _In_ std::optional<DWORD> DesiredAccess = std::nullopt, _In_ BOOL InheritHandle = FALSE);
217
104 -[[nodiscard]] HANDLE DuplicateHandleFromCallingProcess(_In_ HANDLE Handle);
218 +[[nodiscard]] HANDLE DuplicateHandleFromCallingProcess(_In_ HANDLE Handle, _In_ std::optional<DWORD> DesiredAccess = {});
219
106 -[[nodiscard]] HANDLE DuplicateHandleToCallingProcess(_In_ HANDLE Handle, _In_ std::optional<DWORD> Permissions = {});
220 +[[nodiscard]] HANDLE DuplicateHandleToCallingProcess(_In_ HANDLE Handle, _In_ std::optional<DWORD> DesiredAccess = {});
221
222 void EnforceFileLimit(LPCWSTR Folder, size_t limit, const std::function<bool(const std::filesystem::directory_entry&)>& pred);
223
@@ -111,8 +225,12 @@ std::wstring ErrorCodeToString(HRESULT Error);
225
226 ErrorStrings ErrorToString(const Error& error);
227
228 +[[nodiscard]] HANDLE FromCOMInputHandle(WSLCHandle Handle);
229 +
230 std::filesystem::path GetBasePath();
231
232 +std::optional<COMErrorInfo> GetCOMErrorInfo();
233 +
234 DWORD GetDefaultVersion(void);
235
236 std::wstring GetErrorString(_In_ HRESULT result);
@@ -135,6 +253,8 @@ std::wstring GetSystemErrorString(_In_ HRESULT result);
253
254 std::wstring GetDebugShellPipeName(_In_ PSID Sid);
255
256 +std::optional<std::tuple<uint32_t, uint32_t, uint32_t>> GetInstalledPackageVersion();
257 +
258 std::vector<BYTE> HashFile(HANDLE File, DWORD Algorithm);
259
260 void InitializeWil();
@@ -151,12 +271,20 @@ bool IsVirtualMachinePlatformInstalled();
271
272 std::vector<DWORD> ListRunningProcesses();
273
274 +std::pair<std::string, std::string> NormalizeRepo(const std::string& Input);
275 +
276 std::pair<wil::unique_hfile, wil::unique_hfile> OpenAnonymousPipe(DWORD Size, bool ReadPipeOverlapped, bool WritePipeOverlapped);
277
278 wil::unique_handle OpenCallingProcess(_In_ DWORD access);
279
280 +void ParseIpv4Address(const char* Address, in_addr& Result);
281 +
282 +void ParseIpv6Address(const char* Address, in_addr6& Result);
283 +
284 std::tuple<uint32_t, uint32_t, uint32_t> ParseWslPackageVersion(_In_ const std::wstring& Version);
285
286 +std::pair<std::string, std::optional<std::string>> ParseImage(const std::string& Input, EnumReferenceFormat* Format = nullptr);
287 +
288 void PrintSystemError(_In_ HRESULT result, _Inout_ FILE* stream = stdout);
289
290 void PrintMessageImpl(_In_ const std::wstring& message, _In_ va_list& args, _Inout_ FILE* stream = stdout);
@@ -196,6 +324,23 @@ void SetThreadDescription(LPCWSTR Name);
324
325 wil::unique_hlocal_string SidToString(_In_ PSID Sid);
326
327 +WSLCHandle ToCOMInputHandle(HANDLE Handle);
328 +[[nodiscard]] WSLCHandle ToCOMOutputHandle(HANDLE Handle, DWORD Access);
329 +[[nodiscard]] WSLCHandle ToCOMOutputHandle(HANDLE Handle, DWORD Access, WSLCHandleType Type);
330 +
331 winrt::Windows::Management::Deployment::PackageVolume GetSystemVolume();
332
333 +std::string Base64Encode(const std::string& input);
334 +std::string Base64Decode(const std::string& encoded);
335 +
336 +// Builds the base64-encoded X-Registry-Auth header value used by Docker APIs
337 +// (PullImage, PushImage, etc.) from the given credentials.
338 +std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password);
339 +
340 +// Builds the base64-encoded X-Registry-Auth header value from an identity token
341 +// returned by Authenticate().
342 +std::string BuildRegistryAuthHeader(const std::string& identityToken);
343 +
344 +std::map<std::string, std::string> ParseKeyValuePairs(_In_reads_opt_(count) const KeyValuePair* pairs, ULONG count, _In_opt_ LPCSTR reservedKey = nullptr);
345 +
346 } // namespace wsl::windows::common::wslutil
src/windows/inc/WslPluginApi.h
+1 -1
@@ -24,7 +24,7 @@ extern "C" {
24 #endif
25
26 #define WSLPLUGINAPI_ENTRYPOINTV1 WSLPluginAPIV1_EntryPoint
27 -#define WSL_E_PLUGIN_REQUIRES_UPDATE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x8004032A)
27 +#define WSL_E_PLUGIN_REQUIRES_UPDATE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x032A)
28
29 #define WSL_PLUGIN_REQUIRE_VERSION(_Major, _Minor, _Revision, Api) \
30 if (Api->Version.Major < (_Major) || (Api->Version.Major == (_Major) && Api->Version.Minor < (_Minor)) || \
src/windows/inc/docker_schema.h new
+524
@@ -0,0 +1,524 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + docker_schema.h
8 +
9 +Abstract:
10 +
11 + JSON schema for the docker API.
12 + The documentation for the API can be found at: https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Container
13 +
14 +--*/
15 +
16 +#pragma once
17 +
18 +#include "JsonUtils.h"
19 +
20 +namespace wsl::windows::common::docker_schema {
21 +
22 +struct CreatedContainer
23 +{
24 + std::string Id;
25 + std::string Name;
26 + std::vector<std::string> Warnings;
27 +
28 + NLOHMANN_DEFINE_TYPE_INTRUSIVE(CreatedContainer, Id, Warnings);
29 +};
30 +
31 +struct ErrorResponse
32 +{
33 + std::string message;
34 +
35 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ErrorResponse, message);
36 +};
37 +
38 +struct ImageLoadResult
39 +{
40 + std::optional<std::string> stream;
41 + std::optional<ErrorResponse> errorDetail;
42 +
43 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ImageLoadResult, stream, errorDetail);
44 +};
45 +
46 +struct EmptyRequest
47 +{
48 + using TResponse = void;
49 +};
50 +
51 +struct AuthRequest
52 +{
53 + using TResponse = struct AuthResponse;
54 +
55 + std::string username;
56 + std::string password;
57 + std::string serveraddress;
58 +
59 + NLOHMANN_DEFINE_TYPE_INTRUSIVE(AuthRequest, username, password, serveraddress);
60 +};
61 +
62 +struct AuthResponse
63 +{
64 + std::string Status;
65 + std::optional<std::string> IdentityToken;
66 +
67 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(AuthResponse, Status, IdentityToken);
68 +};
69 +
70 +struct VolumeUsageData
71 +{
72 + int64_t Size{-1};
73 + int64_t RefCount{-1};
74 +
75 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(VolumeUsageData, Size, RefCount);
76 +};
77 +
78 +struct Volume
79 +{
80 + std::string Name;
81 + std::string Driver;
82 + std::string Mountpoint;
83 + std::string CreatedAt;
84 + std::optional<std::map<std::string, std::string>> Options;
85 + std::optional<std::map<std::string, std::string>> Labels;
86 + std::optional<std::map<std::string, std::string>> Status;
87 + std::optional<VolumeUsageData> UsageData;
88 +
89 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Volume, Name, Driver, Mountpoint, CreatedAt, Options, Labels, Status, UsageData);
90 +};
91 +
92 +struct CreateVolume
93 +{
94 + using TResponse = Volume;
95 +
96 + std::string Name;
97 + std::string Driver;
98 + std::map<std::string, std::string> DriverOpts;
99 + std::map<std::string, std::string> Labels;
100 +
101 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateVolume, Name, Driver, DriverOpts, Labels);
102 +};
103 +
104 +struct ListVolumesResponse
105 +{
106 + std::vector<Volume> Volumes;
107 +
108 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ListVolumesResponse, Volumes);
109 +};
110 +
111 +struct IPAMConfig
112 +{
113 + std::string Subnet;
114 + std::string Gateway;
115 +
116 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(IPAMConfig, Subnet, Gateway);
117 +};
118 +
119 +struct IPAM
120 +{
121 + std::string Driver;
122 + std::optional<std::vector<IPAMConfig>> Config;
123 +
124 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(IPAM, Driver, Config);
125 +};
126 +
127 +struct CreateNetworkResponse
128 +{
129 + std::string Id;
130 + std::string Warning;
131 +
132 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateNetworkResponse, Id, Warning);
133 +};
134 +
135 +struct CreateNetwork
136 +{
137 + using TResponse = CreateNetworkResponse;
138 +
139 + std::string Name;
140 + std::string Driver;
141 + bool Internal{};
142 + std::optional<IPAM> IPAM;
143 + std::map<std::string, std::string> Labels;
144 +
145 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateNetwork, Name, Driver, Internal, IPAM, Labels);
146 +};
147 +
148 +struct Network
149 +{
150 + std::string Id;
151 + std::string Name;
152 + std::string Driver;
153 + std::string Scope;
154 + bool Internal{};
155 + IPAM IPAM;
156 + std::map<std::string, std::string> Labels;
157 +
158 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Labels);
159 +};
160 +
161 +struct EmptyObject
162 +{
163 +};
164 +
165 +inline void to_json(nlohmann::json& j, const EmptyObject& memory)
166 +{
167 + UNREFERENCED_PARAMETER(memory);
168 + j = nlohmann::json::object();
169 +}
170 +
171 +inline void from_json(const nlohmann::json& j, EmptyObject& obj)
172 +{
173 + // EmptyObject has no fields, so nothing to deserialize
174 + UNREFERENCED_PARAMETER(j);
175 + UNREFERENCED_PARAMETER(obj);
176 +}
177 +
178 +struct Mount
179 +{
180 + std::string Name;
181 + std::string Source;
182 + std::string Target;
183 + std::string Type;
184 + bool ReadOnly{};
185 +
186 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Mount, Name, Target, Source, Type, ReadOnly);
187 +};
188 +
189 +struct PortMapping
190 +{
191 + std::string HostIp;
192 + std::string HostPort;
193 +
194 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(PortMapping, HostIp, HostPort);
195 +};
196 +
197 +struct HostConfig
198 +{
199 + std::vector<Mount> Mounts;
200 + std::map<std::string, std::vector<PortMapping>> PortBindings;
201 + std::string NetworkMode;
202 + bool Init{};
203 + std::optional<std::vector<std::string>> Dns;
204 + std::optional<std::vector<std::string>> DnsSearch;
205 + std::optional<std::vector<std::string>> DnsOptions;
206 + std::optional<std::vector<std::string>> Binds;
207 + std::map<std::string, std::string> Tmpfs;
208 +
209 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HostConfig, Mounts, PortBindings, NetworkMode, Init, Dns, DnsSearch, DnsOptions, Binds, Tmpfs);
210 +};
211 +
212 +struct CreateContainer
213 +{
214 + using TResponse = CreatedContainer;
215 +
216 + std::string Image;
217 + bool Tty{};
218 + bool OpenStdin{};
219 + bool StdinOnce{};
220 + bool AttachStdin{};
221 + bool AttachStdout{};
222 + bool AttachStderr{};
223 + std::optional<std::string> User;
224 + std::string Hostname;
225 + std::string Domainname;
226 + std::optional<std::string> StopSignal;
227 + std::optional<std::string> WorkingDir;
228 + std::optional<std::vector<std::string>> Cmd;
229 + std::optional<std::vector<std::string>> Entrypoint;
230 + std::vector<std::string> Env;
231 + std::map<std::string, EmptyObject> ExposedPorts;
232 + std::map<std::string, std::string> Labels;
233 + HostConfig HostConfig;
234 +
235 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(
236 + CreateContainer, Image, Cmd, Tty, OpenStdin, StdinOnce, Entrypoint, Env, ExposedPorts, HostConfig, StopSignal, WorkingDir, User, Hostname, Domainname, Labels);
237 +};
238 +
239 +struct ContainerInspectState
240 +{
241 + std::string Status;
242 + bool Running{};
243 + int ExitCode{};
244 + std::string StartedAt;
245 + std::string FinishedAt;
246 +
247 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInspectState, Status, Running, ExitCode, StartedAt, FinishedAt);
248 +};
249 +
250 +struct ContainerConfig
251 +{
252 + std::string Image;
253 +
254 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Image);
255 +};
256 +
257 +struct InspectMount
258 +{
259 + std::string Type;
260 + std::string Source;
261 + std::string Destination;
262 + bool RW{};
263 +
264 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectMount, Type, Source, Destination, RW);
265 +};
266 +
267 +struct InspectContainer
268 +{
269 + std::string Id;
270 + std::string Name;
271 + std::string Created;
272 + std::string Image;
273 + ContainerInspectState State;
274 + ContainerConfig Config;
275 + HostConfig HostConfig;
276 +
277 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, Config, HostConfig);
278 +};
279 +
280 +struct InspectExec
281 +{
282 + std::optional<int> Pid{};
283 + std::optional<int> ExitCode{};
284 + bool Running{};
285 +
286 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectExec, Pid, ExitCode, Running);
287 +};
288 +
289 +struct PruneContainerResult
290 +{
291 + std::optional<std::vector<std::string>> ContainersDeleted; // Null if no containers were deleted.
292 + uint64_t SpaceReclaimed{};
293 +
294 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(PruneContainerResult, ContainersDeleted, SpaceReclaimed);
295 +};
296 +
297 +struct Image
298 +{
299 + std::string Id;
300 + std::vector<std::string> RepoTags;
301 + std::vector<std::string> RepoDigests;
302 + uint64_t Size{};
303 + int64_t Created{};
304 + std::string ParentId;
305 +
306 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Image, Id, RepoTags, RepoDigests, Size, Created, ParentId);
307 +};
308 +
309 +struct DeletedImage
310 +{
311 + std::string Untagged;
312 + std::string Deleted;
313 +
314 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(DeletedImage, Untagged, Deleted);
315 +};
316 +
317 +struct PruneImageResult
318 +{
319 + std::optional<std::vector<DeletedImage>> ImagesDeleted;
320 + uint64_t SpaceReclaimed{};
321 +
322 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(PruneImageResult, ImagesDeleted, SpaceReclaimed);
323 +};
324 +
325 +struct ImportStatus
326 +{
327 + std::string status;
328 +
329 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ImportStatus, status);
330 +};
331 +
332 +struct ImageConfig
333 +{
334 + std::string User;
335 + std::optional<std::map<std::string, EmptyObject>> ExposedPorts;
336 + std::optional<std::vector<std::string>> Env;
337 + std::optional<std::vector<std::string>> Cmd;
338 + std::optional<std::vector<std::string>> Entrypoint;
339 + std::optional<std::map<std::string, EmptyObject>> Volumes;
340 + std::string WorkingDir;
341 + std::optional<std::map<std::string, std::string>> Labels;
342 + std::string StopSignal;
343 + std::optional<std::vector<std::string>> Shell;
344 +
345 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ImageConfig, User, ExposedPorts, Env, Cmd, Entrypoint, Volumes, WorkingDir, Labels, StopSignal, Shell);
346 +};
347 +
348 +struct RootFS
349 +{
350 + std::string Type;
351 + std::optional<std::vector<std::string>> Layers;
352 +
353 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(RootFS, Type, Layers);
354 +};
355 +
356 +struct GraphDriverData
357 +{
358 + std::string Name;
359 + std::optional<std::map<std::string, std::string>> Data;
360 +
361 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GraphDriverData, Name, Data);
362 +};
363 +
364 +struct InspectImage
365 +{
366 + std::string Id;
367 + std::optional<std::vector<std::string>> RepoTags;
368 + std::optional<std::vector<std::string>> RepoDigests;
369 + std::string Parent;
370 + std::string Comment;
371 + std::string Created;
372 + std::optional<ImageConfig> Config;
373 + std::string Author;
374 + std::string Architecture;
375 + std::string Variant;
376 + std::string Os;
377 + std::string OsVersion;
378 + uint64_t Size{};
379 + std::optional<GraphDriverData> GraphDriver;
380 + std::optional<RootFS> RootFS;
381 + std::optional<std::map<std::string, std::string>> Metadata;
382 +
383 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
384 + InspectImage, Id, RepoTags, RepoDigests, Parent, Comment, Created, Config, Author, Architecture, Variant, Os, OsVersion, Size, GraphDriver, RootFS, Metadata);
385 +};
386 +
387 +struct CreateExecResponse
388 +{
389 + std::string Id;
390 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateExecResponse, Id);
391 +};
392 +
393 +struct CreateExec
394 +{
395 + using TResponse = CreateExecResponse;
396 +
397 + bool AttachStdin{};
398 + bool AttachStdout{};
399 + bool AttachStderr{};
400 + bool Tty{};
401 + std::vector<ULONG> ConsoleSize;
402 + std::vector<std::string> Cmd;
403 + std::vector<std::string> Env;
404 + std::optional<std::string> User;
405 + std::string WorkingDir;
406 + std::optional<std::string> DetachKeys;
407 +
408 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateExec, AttachStdin, AttachStdout, AttachStderr, Tty, ConsoleSize, Cmd, Env, WorkingDir, User, DetachKeys);
409 +};
410 +
411 +struct StartExec
412 +{
413 + using TResponse = void;
414 + bool Tty{};
415 + bool Detach{};
416 + std::vector<ULONG> ConsoleSize;
417 +
418 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(StartExec, Tty, Detach, ConsoleSize);
419 +};
420 +
421 +enum class ContainerState
422 +{
423 + Created,
424 + Running,
425 + Paused,
426 + Restarting,
427 + Exited,
428 + Removing,
429 + Dead,
430 + Unknown
431 +};
432 +
433 +NLOHMANN_JSON_SERIALIZE_ENUM(
434 + ContainerState,
435 + {
436 + {ContainerState::Created, "created"},
437 + {ContainerState::Running, "running"},
438 + {ContainerState::Paused, "paused"},
439 + {ContainerState::Restarting, "restarting"},
440 + {ContainerState::Exited, "exited"},
441 + {ContainerState::Removing, "removing"},
442 + {ContainerState::Dead, "dead"},
443 + });
444 +
445 +struct Port
446 +{
447 + uint16_t PrivatePort{};
448 + uint16_t PublicPort{};
449 +
450 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Port, PrivatePort, PublicPort);
451 +};
452 +
453 +struct ContainerInfo
454 +{
455 + std::string Id;
456 + std::vector<std::string> Names;
457 + std::string Image;
458 + std::map<std::string, std::string> Labels;
459 + std::vector<Port> Ports;
460 + std::vector<Mount> Mounts;
461 + ContainerState State{ContainerState::Unknown};
462 + int64_t Created{};
463 + HostConfig HostConfig;
464 +
465 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInfo, Id, Names, Image, Labels, Ports, Mounts, State, Created, HostConfig);
466 +};
467 +
468 +struct BuildKitVertex
469 +{
470 + std::string digest;
471 + std::string name;
472 + std::string started;
473 + std::string error;
474 +
475 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(BuildKitVertex, digest, name, started, error);
476 +};
477 +
478 +struct BuildKitStatus
479 +{
480 + std::string id;
481 + std::string vertex;
482 +
483 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(BuildKitStatus, id, vertex);
484 +};
485 +
486 +struct BuildKitLog
487 +{
488 + std::string vertex;
489 + std::string data; // base64-encoded output
490 + int stream{}; // 1 = stdout, 2 = stderr
491 +
492 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(BuildKitLog, vertex, data, stream);
493 +};
494 +
495 +struct BuildKitSolveStatus
496 +{
497 + std::vector<BuildKitVertex> vertexes;
498 + std::vector<BuildKitStatus> statuses;
499 + std::vector<BuildKitLog> logs;
500 +
501 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(BuildKitSolveStatus, vertexes, statuses, logs);
502 +};
503 +
504 +struct CreateImageProgressDetails
505 +{
506 + uint64_t current{};
507 + uint64_t total{};
508 + std::string unit;
509 +
510 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateImageProgressDetails, current, total, unit);
511 +};
512 +
513 +struct CreateImageProgress
514 +{
515 + std::string status;
516 + std::string id;
517 + std::optional<ErrorResponse> errorDetail;
518 +
519 + CreateImageProgressDetails progressDetail;
520 +
521 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateImageProgress, status, id, progressDetail, errorDetail);
522 +};
523 +
524 +} // namespace wsl::windows::common::docker_schema
\ No newline at end of file
src/windows/inc/wslc_schema.h new
+146
@@ -0,0 +1,146 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + wslc_schema.h
8 +
9 +Abstract:
10 +
11 + Contains the WSLC schema definitions for container operations.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "JsonUtils.h"
18 +
19 +namespace wsl::windows::common::wslc_schema {
20 +
21 +struct InspectPortBinding
22 +{
23 + // WSLC always binds to localhost. Included for Docker API compatibility.
24 + std::string HostIp = "127.0.0.1";
25 + std::string HostPort;
26 +
27 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectPortBinding, HostIp, HostPort);
28 +};
29 +
30 +struct InspectMount
31 +{
32 + // TODO: Support different mount types (plan9/VHD) when VHD volumes are implemented.
33 + std::string Type;
34 + std::string Source;
35 + std::string Destination;
36 + bool ReadWrite{};
37 +
38 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectMount, Type, Source, Destination, ReadWrite);
39 +};
40 +
41 +struct InspectState
42 +{
43 + std::string Status;
44 + bool Running{};
45 + int ExitCode{};
46 + std::string StartedAt;
47 + std::string FinishedAt;
48 +
49 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectState, Status, Running, ExitCode, StartedAt, FinishedAt);
50 +};
51 +
52 +struct InspectHostConfig
53 +{
54 + std::string NetworkMode;
55 +
56 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectHostConfig, NetworkMode);
57 +};
58 +
59 +struct InspectContainer
60 +{
61 + std::string Id;
62 + std::string Name;
63 + std::string Created;
64 + std::string Image;
65 + InspectState State;
66 + InspectHostConfig HostConfig;
67 + std::map<std::string, std::vector<InspectPortBinding>> Ports;
68 + std::vector<InspectMount> Mounts;
69 + std::map<std::string, std::string> Labels;
70 +
71 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, HostConfig, Ports, Mounts, Labels);
72 +};
73 +
74 +struct ImageConfig
75 +{
76 + std::optional<std::vector<std::string>> Cmd;
77 + std::optional<std::vector<std::string>> Entrypoint;
78 + std::optional<std::vector<std::string>> Env;
79 + std::optional<std::map<std::string, std::string>> Labels;
80 + std::string User;
81 + std::string WorkingDir;
82 +
83 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ImageConfig, Cmd, Entrypoint, Env, Labels, User, WorkingDir);
84 +};
85 +
86 +struct InspectImage
87 +{
88 + std::string Id;
89 + std::optional<std::vector<std::string>> RepoTags;
90 + std::optional<std::vector<std::string>> RepoDigests;
91 + std::string Parent;
92 + std::string Comment;
93 + std::string Created;
94 + std::string Author;
95 + std::string Architecture;
96 + std::string Os;
97 + uint64_t Size{};
98 + std::optional<std::map<std::string, std::string>> Metadata;
99 + std::optional<ImageConfig> Config;
100 +
101 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
102 + InspectImage, Id, RepoTags, RepoDigests, Parent, Comment, Created, Author, Architecture, Os, Size, Metadata, Config);
103 +};
104 +
105 +struct InspectVolume
106 +{
107 + std::string Name;
108 + std::string Driver;
109 + std::string CreatedAt;
110 + std::map<std::string, std::string> DriverOpts;
111 + std::map<std::string, std::string> Labels;
112 + std::optional<std::map<std::string, std::string>> Status;
113 +
114 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectVolume, Name, Driver, CreatedAt, DriverOpts, Labels, Status);
115 +};
116 +
117 +struct InspectIPAMConfig
118 +{
119 + std::string Subnet;
120 + std::string Gateway;
121 +
122 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectIPAMConfig, Subnet, Gateway);
123 +};
124 +
125 +struct InspectIPAM
126 +{
127 + std::string Driver;
128 + std::optional<std::vector<InspectIPAMConfig>> Config;
129 +
130 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectIPAM, Driver, Config);
131 +};
132 +
133 +struct InspectNetwork
134 +{
135 + std::string Id;
136 + std::string Name;
137 + std::string Driver;
138 + std::string Scope;
139 + bool Internal{};
140 + InspectIPAM IPAM;
141 + std::map<std::string, std::string> Labels;
142 +
143 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectNetwork, Id, Name, Driver, Scope, Internal, IPAM, Labels);
144 +};
145 +
146 +} // namespace wsl::windows::common::wslc_schema
src/windows/inc/wslrelay.h
+1
@@ -20,6 +20,7 @@ enum RelayMode
20 Invalid = -1,
21 DebugConsole,
22 PortRelay,
23 + WSLCPortRelay,
24 KdRelay
25 };
26
src/windows/service/exe/CMakeLists.txt
+11 -4
@@ -20,6 +20,9 @@ set(SOURCES
20 WslMirroredNetworking.cpp
21 WslCoreTcpIpStateTracking.cpp
22 WslCoreVm.cpp
23 + HcsVirtualMachine.cpp
24 + WSLCSessionManager.cpp
25 + WSLCSessionManagerFactory.cpp
26 main.rc
27 ${CMAKE_CURRENT_BINARY_DIR}/../mc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE}/wsleventschema.rc
28 application.manifest)
@@ -47,14 +50,17 @@ set(HEADERS
50 WslMirroredNetworking.h
51 WslCoreNetworkEndpoint.h
52 WslCoreTcpIpStateTracking.h
50 - WslCoreVm.h)
53 + WslCoreVm.h
54 + HcsVirtualMachine.h
55 + WSLCSessionManager.h
56 + WSLCSessionManagerFactory.h)
57
58 add_executable(wslservice ${SOURCES} ${HEADERS})
59 add_dependencies(wslservice wslserviceidl wslservicemc)
60 add_compile_definitions(__WRL_CLASSIC_COM__)
61 add_compile_definitions(__WRL_DISABLE_STATIC_INITIALIZE__)
62 add_compile_definitions(USE_COM_CONTEXT_DEF=1)
57 -set_target_properties(wslservice PROPERTIES LINK_FLAGS "/merge:minATL=.rdata /include:__minATLObjMap_LxssUserSession_COM")
63 +set_target_properties(wslservice PROPERTIES LINK_FLAGS "/merge:minATL=.rdata /include:__minATLObjMap_LxssUserSession_COM /include:__minATLObjMap_WSLCSessionManager_COM")
64 target_link_libraries(wslservice
65 ${COMMON_LINK_LIBRARIES}
66 ${MSI_LINK_LIBRARIES}
@@ -65,7 +71,8 @@ target_link_libraries(wslservice
71 legacy_stdio_definitions
72 VirtDisk.lib
73 Winhttp.lib
68 - Synchronization.lib)
74 + Synchronization.lib
75 + yaml-cpp)
76
77 target_precompile_headers(wslservice REUSE_FROM common)
71 -set_target_properties(wslservice PROPERTIES FOLDER windows)
\ No newline at end of file
78 +set_target_properties(wslservice PROPERTIES FOLDER windows)
src/windows/service/exe/HcsVirtualMachine.cpp new
+759
@@ -0,0 +1,759 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + HcsVirtualMachine.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of IWSLCVirtualMachine - represents a single HCS-based VM instance.
12 +
13 +--*/
14 +
15 +#include "HcsVirtualMachine.h"
16 +#include <format>
17 +#include "hcs_schema.h"
18 +#include "VirtioNetworking.h"
19 +#include "NatNetworking.h"
20 +#include "wslsecurity.h"
21 +#include "wslutil.h"
22 +#include "lxinitshared.h"
23 +#include "DnsResolver.h"
24 +
25 +using namespace wsl::windows::common;
26 +using helpers::WindowsBuildNumbers;
27 +using wsl::windows::service::wslc::HcsVirtualMachine;
28 +
29 +constexpr auto MAX_VM_CRASH_FILES = 3;
30 +constexpr auto SAVED_STATE_FILE_EXTENSION = L".vmrs";
31 +constexpr auto SAVED_STATE_FILE_PREFIX = L"saved-state-";
32 +
33 +HcsVirtualMachine::HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings)
34 +{
35 + THROW_HR_IF(E_POINTER, Settings == nullptr);
36 +
37 + // Store the user token.
38 + m_userToken = wil::shared_handle{wsl::windows::common::security::GetUserToken(TokenImpersonation).release()};
39 + m_virtioFsClassId = wsl::windows::common::security::IsTokenElevated(m_userToken.get()) ? VIRTIO_FS_ADMIN_CLASS_ID : VIRTIO_FS_CLASS_ID;
40 + m_crashDumpFolder = GetCrashDumpFolder();
41 +
42 + std::lock_guard lock(m_lock);
43 +
44 + THROW_IF_FAILED(CoCreateGuid(&m_vmId));
45 + m_vmIdString = wsl::shared::string::GuidToString<wchar_t>(m_vmId, wsl::shared::string::GuidToStringFlags::Uppercase);
46 + m_featureFlags = Settings->FeatureFlags;
47 + m_networkingMode = Settings->NetworkingMode;
48 + m_bootTimeoutMs = Settings->BootTimeoutMs;
49 +
50 + // Build HCS settings
51 + hcs::ComputeSystem systemSettings{};
52 + systemSettings.Owner = Settings->DisplayName ? Settings->DisplayName : L"WSLC";
53 + systemSettings.ShouldTerminateOnLastHandleClosed = true;
54 +
55 + // Determine which schema version to use based on the Windows version. Windows 10 does not support
56 + // newer schema versions and some features may be disabled as a result.
57 + if (wsl::windows::common::helpers::IsWindows11OrAbove())
58 + {
59 + systemSettings.SchemaVersion.Major = 2;
60 + systemSettings.SchemaVersion.Minor = 7;
61 + }
62 + else
63 + {
64 + systemSettings.SchemaVersion.Major = 2;
65 + systemSettings.SchemaVersion.Minor = 3;
66 + }
67 +
68 + hcs::VirtualMachine vmSettings{};
69 + vmSettings.StopOnReset = true;
70 + vmSettings.Chipset.UseUtc = true;
71 +
72 + // Ensure the 2MB granularity enforced by HCS.
73 + vmSettings.ComputeTopology.Memory.SizeInMB = Settings->MemoryMb & ~0x1;
74 + vmSettings.ComputeTopology.Memory.AllowOvercommit = true;
75 + vmSettings.ComputeTopology.Memory.EnableDeferredCommit = true;
76 + vmSettings.ComputeTopology.Memory.EnableColdDiscardHint = true;
77 + vmSettings.ComputeTopology.Processor.Count = Settings->CpuCount;
78 +
79 + // Configure backing page size, fault cluster shift size, and cold discard hint size to favor density (lower vmmem usage).
80 + //
81 + // N.B. Cold discard hint size should be a multiple of the fault cluster shift size.
82 + const auto windowsVersion = wsl::windows::common::helpers::GetWindowsVersion();
83 + if (windowsVersion.BuildNumber >= WindowsBuildNumbers::Germanium)
84 + {
85 + vmSettings.ComputeTopology.Memory.BackingPageSize = hcs::MemoryBackingPageSize::Small;
86 + vmSettings.ComputeTopology.Memory.FaultClusterSizeShift = 4;
87 + vmSettings.ComputeTopology.Memory.DirectMapFaultClusterSizeShift = 4;
88 + }
89 +
90 + if (helpers::IsVmemmSuffixSupported() && Settings->DisplayName)
91 + {
92 + vmSettings.ComputeTopology.Memory.HostingProcessNameSuffix = Settings->DisplayName;
93 + }
94 +
95 +#ifdef _AMD64_
96 +
97 + HV_X64_HYPERVISOR_HARDWARE_FEATURES hardwareFeatures{};
98 + __cpuid(reinterpret_cast<int*>(&hardwareFeatures), HvCpuIdFunctionMsHvHardwareFeatures);
99 + vmSettings.ComputeTopology.Processor.EnablePerfmonPmu = hardwareFeatures.ChildPerfmonPmuSupported != 0;
100 + vmSettings.ComputeTopology.Processor.EnablePerfmonLbr = hardwareFeatures.ChildPerfmonLbrSupported != 0;
101 +
102 +#endif
103 +
104 + // Initialize kernel command line.
105 + std::wstring kernelCmdLine = L"initrd=\\" LXSS_VM_MODE_INITRD_NAME L" " TEXT(WSLC_ROOT_INIT_ENV) L"=1 panic=-1";
106 + kernelCmdLine += std::format(L" nr_cpus={}", Settings->CpuCount);
107 +
108 + // Enable timesync workaround to sync on resume from sleep in modern standby.
109 + kernelCmdLine += L" hv_utils.timesync_implicit=1";
110 +
111 + // Setup dmesg collector with optional DmesgOutput handle.
112 + // TODO: move dmesg collector to user session process.
113 + // N.B. 'DmesgOutput' needs to be duplicated since COM will close it when this call completes.
114 + wil::unique_handle dmesgOutputHandle;
115 + if (Settings->DmesgOutput.Handle.File != nullptr && Settings->DmesgOutput.Handle.File != INVALID_HANDLE_VALUE)
116 + {
117 + dmesgOutputHandle.reset(wslutil::DuplicateHandle(wslutil::FromCOMInputHandle(Settings->DmesgOutput), GENERIC_WRITE | SYNCHRONIZE));
118 + }
119 +
120 + m_dmesgCollector = DmesgCollector::Create(
121 + m_vmId, m_vmExitEvent, true, false, L"", FeatureEnabled(WslcFeatureFlagsEarlyBootDmesg), std::move(dmesgOutputHandle));
122 +
123 + if (FeatureEnabled(WslcFeatureFlagsEarlyBootDmesg))
124 + {
125 + kernelCmdLine += L" earlycon=uart8250,io,0x3f8,115200";
126 + vmSettings.Devices.ComPorts["0"] = hcs::ComPort{m_dmesgCollector->EarlyConsoleName()};
127 + }
128 +
129 + if (helpers::IsVirtioSerialConsoleSupported())
130 + {
131 + kernelCmdLine += L" console=hvc0 debug";
132 + vmSettings.Devices.VirtioSerial.emplace();
133 + hcs::VirtioSerialPort virtioPort{};
134 + virtioPort.Name = L"hvc0";
135 + virtioPort.NamedPipe = m_dmesgCollector->VirtioConsoleName();
136 + virtioPort.ConsoleSupport = true;
137 + vmSettings.Devices.VirtioSerial->Ports["0"] = std::move(virtioPort);
138 + }
139 +
140 + // Set up boot params.
141 + //
142 + // N.B. Linux kernel direct boot is not yet supported on ARM64.
143 + auto basePath = wslutil::GetBasePath();
144 +
145 +#ifdef WSL_KERNEL_PATH
146 + auto kernelPath = std::filesystem::path(WSL_KERNEL_PATH);
147 +#else
148 + auto kernelPath = std::filesystem::path(basePath) / L"tools" / LXSS_VM_MODE_KERNEL_NAME;
149 +#endif
150 +
151 + if constexpr (!wsl::shared::Arm64)
152 + {
153 + vmSettings.Chipset.LinuxKernelDirect.emplace();
154 + vmSettings.Chipset.LinuxKernelDirect->KernelFilePath = kernelPath.wstring();
155 + vmSettings.Chipset.LinuxKernelDirect->InitRdPath = (basePath / L"tools" / LXSS_VM_MODE_INITRD_NAME).c_str();
156 + vmSettings.Chipset.LinuxKernelDirect->KernelCmdLine = kernelCmdLine;
157 + }
158 + else
159 + {
160 + auto bootThis = hcs::UefiBootEntry{};
161 + bootThis.DeviceType = hcs::UefiBootDevice::VmbFs;
162 + bootThis.VmbFsRootPath = (basePath / L"tools").c_str();
163 + bootThis.DevicePath = L"\\" LXSS_VM_MODE_KERNEL_NAME;
164 + bootThis.OptionalData = kernelCmdLine;
165 + hcs::Uefi uefiSettings{};
166 + uefiSettings.BootThis = std::move(bootThis);
167 + vmSettings.Chipset.Uefi = std::move(uefiSettings);
168 + }
169 +
170 +#ifdef WSL_KERNEL_MODULES_PATH
171 + auto kernelModulesPath = std::filesystem::path(TEXT(WSL_KERNEL_MODULES_PATH));
172 +#else
173 + auto kernelModulesPath = basePath / L"tools" / L"modules.vhd";
174 +#endif
175 +
176 + // Get root VHD path
177 + std::filesystem::path rootVhdPath;
178 + if (Settings->RootVhdOverride != nullptr)
179 + {
180 + rootVhdPath = Settings->RootVhdOverride;
181 + }
182 + else
183 + {
184 +#ifdef WSL_SYSTEM_DISTRO_PATH
185 + rootVhdPath = TEXT(WSL_SYSTEM_DISTRO_PATH);
186 +#else
187 + rootVhdPath = std::filesystem::path(wslutil::GetMsiPackagePath().value()) / L"system.vhd";
188 +#endif
189 + }
190 +
191 + // Setup boot VHDs
192 + hcs::Scsi scsiController{};
193 + auto attachScsiDisk = [&](PCWSTR path) {
194 + const ULONG lun = AllocateLun();
195 + hcs::Attachment disk{};
196 + disk.Type = hcs::AttachmentType::VirtualDisk;
197 + disk.Path = path;
198 + disk.ReadOnly = true;
199 + disk.SupportCompressedVolumes = true;
200 + disk.AlwaysAllowSparseFiles = true;
201 + disk.SupportEncryptedFiles = true;
202 + scsiController.Attachments[std::to_string(lun)] = std::move(disk);
203 + DiskInfo diskInfo{path};
204 + m_attachedDisks.emplace(lun, std::move(diskInfo));
205 + };
206 +
207 + attachScsiDisk(rootVhdPath.c_str());
208 + attachScsiDisk(kernelModulesPath.c_str());
209 +
210 + vmSettings.Devices.Scsi["0"] = std::move(scsiController);
211 +
212 + // Setup HvSocket security
213 + auto tokenUser = wil::get_token_information<TOKEN_USER>(m_userToken.get());
214 + wil::unique_hlocal_string userSidString;
215 + THROW_LAST_ERROR_IF(!ConvertSidToStringSidW(tokenUser->User.Sid, &userSidString));
216 +
217 + std::wstring securityDescriptor = std::format(L"D:P(A;;FA;;;SY)(A;;FA;;;{})", userSidString.get());
218 + hcs::HvSocket hvSocketConfig{};
219 + hvSocketConfig.HvSocketConfig.DefaultBindSecurityDescriptor = securityDescriptor;
220 + hvSocketConfig.HvSocketConfig.DefaultConnectSecurityDescriptor = securityDescriptor;
221 + vmSettings.Devices.HvSocket = std::move(hvSocketConfig);
222 +
223 + // Enable .vmrs dump collection if supported.
224 + if (wsl::windows::common::helpers::IsWindows11OrAbove())
225 + {
226 + CreateVmSavedStateFile(m_userToken.get());
227 + if (!m_vmSavedStateFile.empty())
228 + {
229 + hcs::DebugOptions debugOptions{};
230 + debugOptions.BugcheckSavedStateFileName = m_vmSavedStateFile;
231 + vmSettings.DebugOptions = std::move(debugOptions);
232 + }
233 + }
234 +
235 + systemSettings.VirtualMachine = std::move(vmSettings);
236 + auto json = wsl::shared::ToJsonW(systemSettings);
237 +
238 + WSL_LOG("CreateWSLCVirtualMachine", TraceLoggingValue(json.c_str(), "json"));
239 +
240 + // Create and start compute system
241 + m_computeSystem = hcs::CreateComputeSystem(m_vmIdString.c_str(), json.c_str());
242 +
243 + if (FeatureEnabled(WslcFeatureFlagsVirtioFs) || m_networkingMode == WSLCNetworkingModeVirtioProxy)
244 + {
245 + m_guestDeviceManager = std::make_shared<::GuestDeviceManager>(m_vmIdString, m_vmId);
246 + }
247 +
248 + // Configure termination callback
249 + if (Settings->TerminationCallback)
250 + {
251 + m_terminationCallback = Settings->TerminationCallback;
252 + }
253 +
254 + hcs::RegisterCallback(m_computeSystem.get(), &HcsVirtualMachine::OnVmExitCallback, this);
255 +
256 + // Create a listening socket for mini_init to connect to once the VM is running.
257 + m_listenSocket = wsl::windows::common::hvsocket::Listen(m_vmId, LX_INIT_UTILITY_VM_INIT_PORT);
258 +
259 + // Start the virtual machine
260 + hcs::StartComputeSystem(m_computeSystem.get(), json.c_str());
261 +
262 + // Add GPU to the VM if requested
263 + if (FeatureEnabled(WslcFeatureFlagsGPU))
264 + {
265 + hcs::ModifySettingRequest<hcs::GpuConfiguration> gpuRequest{};
266 + gpuRequest.ResourcePath = L"VirtualMachine/ComputeTopology/Gpu";
267 + gpuRequest.RequestType = hcs::ModifyRequestType::Update;
268 + gpuRequest.Settings.AssignmentMode = hcs::GpuAssignmentMode::Mirror;
269 + gpuRequest.Settings.AllowVendorExtension = true;
270 + if (wsl::windows::common::hcs::IsDisableVgpuSettingsSupported())
271 + {
272 + gpuRequest.Settings.DisableGdiAcceleration = true;
273 + gpuRequest.Settings.DisablePresentation = true;
274 + }
275 +
276 + hcs::ModifyComputeSystem(m_computeSystem.get(), wsl::shared::ToJsonW(gpuRequest).c_str());
277 + }
278 +}
279 +
280 +HcsVirtualMachine::~HcsVirtualMachine()
281 +{
282 + std::lock_guard lock(m_lock);
283 +
284 + // Wait up to 5 seconds for the VM to terminate gracefully.
285 + bool forceTerminate = false;
286 + if (!m_vmExitEvent.wait(5000))
287 + {
288 + forceTerminate = true;
289 + try
290 + {
291 + hcs::TerminateComputeSystem(m_computeSystem.get());
292 + }
293 + CATCH_LOG()
294 + }
295 +
296 + WSL_LOG("WSLCTerminateVm", TraceLoggingValue(forceTerminate, "forced"));
297 +
298 + // N.B. Destruction order matters: the networking engine and device manager must be torn down
299 + // before the compute system handle is closed. The networking engine holds a shared_ptr to
300 + // GuestDeviceManager, so it must be released first for the device manager reset to be effective.
301 + m_networkEngine.reset();
302 + m_guestDeviceManager.reset();
303 + m_computeSystem.reset();
304 +
305 + // Revoke VM access for attached disks
306 + for (const auto& e : m_attachedDisks)
307 + {
308 + try
309 + {
310 + if (e.second.AccessGranted)
311 + {
312 + hcs::RevokeVmAccess(m_vmIdString.c_str(), e.second.Path.c_str());
313 + }
314 + }
315 + CATCH_LOG()
316 + }
317 +
318 + // If the VM did not crash, the saved state file should be empty, so we can remove it.
319 + if (!m_vmSavedStateFile.empty() && !m_vmSavedStateCaptured)
320 + {
321 + try
322 + {
323 + WI_ASSERT(std::filesystem::is_empty(m_vmSavedStateFile));
324 + std::filesystem::remove(m_vmSavedStateFile);
325 + }
326 + CATCH_LOG()
327 + }
328 +}
329 +
330 +bool HcsVirtualMachine::FeatureEnabled(WSLCFeatureFlags Value) const
331 +{
332 + return static_cast<ULONG>(m_featureFlags) & static_cast<ULONG>(Value);
333 +}
334 +
335 +HRESULT HcsVirtualMachine::GetId(_Out_ GUID* VmId)
336 +try
337 +{
338 + *VmId = m_vmId;
339 + return S_OK;
340 +}
341 +CATCH_RETURN()
342 +
343 +HRESULT HcsVirtualMachine::AcceptConnection(_Out_ HANDLE* Socket)
344 +try
345 +{
346 + auto socket = wsl::windows::common::hvsocket::CancellableAccept(m_listenSocket.get(), m_bootTimeoutMs, m_vmExitEvent.get());
347 + THROW_HR_IF(E_ABORT, !socket.has_value());
348 +
349 + *Socket = reinterpret_cast<HANDLE>(socket->release());
350 + return S_OK;
351 +}
352 +CATCH_RETURN()
353 +
354 +HRESULT HcsVirtualMachine::ConfigureNetworking(_In_ HANDLE GnsSocket, _In_opt_ HANDLE* DnsSocket)
355 +try
356 +{
357 + std::lock_guard lock(m_lock);
358 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_networkEngine != nullptr);
359 +
360 + if (m_networkingMode == WSLCNetworkingModeNone)
361 + {
362 + return S_OK;
363 + }
364 +
365 + // Duplicate the socket handles - COM manages the lifetime of the marshalled handles,
366 + // so we need our own copies to take ownership.
367 + wil::unique_socket gnsSocketHandle{reinterpret_cast<SOCKET>(wslutil::DuplicateHandle(GnsSocket))};
368 + wil::unique_socket dnsSocketHandle;
369 + if (FeatureEnabled(WslcFeatureFlagsDnsTunneling))
370 + {
371 + THROW_HR_IF(E_INVALIDARG, DnsSocket == nullptr);
372 +
373 + const auto result = wsl::core::networking::DnsResolver::LoadDnsResolverMethods();
374 + if (FAILED(result))
375 + {
376 + LOG_HR_MSG(result, "Failed to load DNS resolver methods, DNS tunneling will be disabled");
377 + WI_ClearFlag(m_featureFlags, WslcFeatureFlagsDnsTunneling);
378 + }
379 + else
380 + {
381 + dnsSocketHandle.reset(reinterpret_cast<SOCKET>(wslutil::DuplicateHandle(*DnsSocket)));
382 + }
383 + }
384 + else
385 + {
386 + THROW_HR_IF(E_INVALIDARG, DnsSocket != nullptr);
387 + }
388 +
389 + if (m_networkingMode == WSLCNetworkingModeNAT)
390 + {
391 + // TODO: refactor this to avoid using wsl config
392 + m_natConfig.emplace(nullptr);
393 + if (!wsl::core::NatNetworking::IsHyperVFirewallSupported(*m_natConfig))
394 + {
395 + m_natConfig->FirewallConfig.reset();
396 + }
397 +
398 + // Enable DNS tunneling if a DNS socket was provided
399 + if (FeatureEnabled(WslcFeatureFlagsDnsTunneling))
400 + {
401 + WI_ASSERT(dnsSocketHandle);
402 +
403 + m_natConfig->EnableDnsTunneling = true;
404 + in_addr address{};
405 + WI_VERIFY(inet_pton(AF_INET, LX_INIT_DNS_TUNNELING_IP_ADDRESS, &address) == 1);
406 + m_natConfig->DnsTunnelingIpAddress = address.S_un.S_addr;
407 + }
408 +
409 + m_networkEngine = std::make_unique<wsl::core::NatNetworking>(
410 + m_computeSystem.get(),
411 + wsl::core::NatNetworking::CreateNetwork(*m_natConfig),
412 + wsl::core::GnsChannel(std::move(gnsSocketHandle)),
413 + *m_natConfig,
414 + std::move(dnsSocketHandle),
415 + nullptr);
416 + }
417 + else if (m_networkingMode == WSLCNetworkingModeVirtioProxy)
418 + {
419 + wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::Ipv6;
420 + if (FeatureEnabled(WslcFeatureFlagsDnsTunneling))
421 + {
422 + WI_SetFlag(flags, wsl::core::VirtioNetworkingFlags::DnsTunnelingSocket);
423 + }
424 +
425 + m_networkEngine = std::make_unique<wsl::core::VirtioNetworking>(
426 + wsl::core::GnsChannel(std::move(gnsSocketHandle)), flags, nullptr, m_guestDeviceManager, m_userToken, std::move(dnsSocketHandle));
427 + }
428 + else
429 + {
430 + THROW_HR_MSG(E_INVALIDARG, "Invalid networking mode: %lu", m_networkingMode);
431 + }
432 +
433 + m_networkEngine->Initialize();
434 +
435 + return S_OK;
436 +}
437 +CATCH_RETURN()
438 +
439 +HRESULT HcsVirtualMachine::AttachDisk(_In_ LPCWSTR Path, _In_ BOOL ReadOnly, _Out_ ULONG* Lun)
440 +try
441 +{
442 + RETURN_HR_IF(E_POINTER, Path == nullptr || Lun == nullptr);
443 +
444 + std::lock_guard lock(m_lock);
445 +
446 + DiskInfo disk{Path};
447 + const ULONG allocatedLun = AllocateLun();
448 +
449 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
450 + if (disk.AccessGranted)
451 + {
452 + hcs::RevokeVmAccess(m_vmIdString.c_str(), disk.Path.c_str());
453 + }
454 +
455 + FreeLun(allocatedLun);
456 + });
457 +
458 + auto grantDiskAccess = [&]() {
459 + auto runAsUser = wil::impersonate_token(m_userToken.get());
460 + hcs::GrantVmAccess(m_vmIdString.c_str(), Path);
461 + disk.AccessGranted = true;
462 + };
463 +
464 + if (!ReadOnly)
465 + {
466 + grantDiskAccess();
467 + }
468 +
469 + auto result = wil::ResultFromException([&]() { hcs::AddVhd(m_computeSystem.get(), Path, allocatedLun, ReadOnly); });
470 +
471 + if (result == HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED) && !disk.AccessGranted)
472 + {
473 + grantDiskAccess();
474 + hcs::AddVhd(m_computeSystem.get(), Path, allocatedLun, ReadOnly);
475 + }
476 + else
477 + {
478 + THROW_IF_FAILED(result);
479 + }
480 +
481 + m_attachedDisks.emplace(allocatedLun, std::move(disk));
482 +
483 + cleanup.release();
484 +
485 + *Lun = allocatedLun;
486 + return S_OK;
487 +}
488 +CATCH_RETURN()
489 +
490 +HRESULT HcsVirtualMachine::DetachDisk(_In_ ULONG Lun)
491 +try
492 +{
493 + std::lock_guard lock(m_lock);
494 +
495 + auto it = m_attachedDisks.find(Lun);
496 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_attachedDisks.end());
497 +
498 + hcs::RemoveScsiDisk(m_computeSystem.get(), Lun);
499 +
500 + FreeLun(Lun);
501 +
502 + if (it->second.AccessGranted)
503 + {
504 + hcs::RevokeVmAccess(m_vmIdString.c_str(), it->second.Path.c_str());
505 + }
506 +
507 + m_attachedDisks.erase(it);
508 +
509 + return S_OK;
510 +}
511 +CATCH_RETURN()
512 +
513 +HRESULT HcsVirtualMachine::AddShare(_In_ LPCWSTR WindowsPath, _In_ BOOL ReadOnly, _Out_ GUID* ShareId)
514 +try
515 +{
516 + RETURN_HR_IF(E_POINTER, WindowsPath == nullptr || ShareId == nullptr);
517 +
518 + std::lock_guard lock(m_lock);
519 +
520 + GUID shareIdLocal;
521 + THROW_IF_FAILED(CoCreateGuid(&shareIdLocal));
522 + auto shareName = wsl::shared::string::GuidToString<wchar_t>(shareIdLocal, wsl::shared::string::None);
523 +
524 + // Add the share entry upfront so the emplace cannot fail after the device is created.
525 + auto it = m_shares.emplace(shareIdLocal, std::nullopt).first;
526 + auto cleanup = wil::scope_exit([&]() { m_shares.erase(it); });
527 +
528 + if (!FeatureEnabled(WslcFeatureFlagsVirtioFs))
529 + {
530 + auto flags = hcs::Plan9ShareFlags::AllowOptions;
531 + WI_SetFlagIf(flags, hcs::Plan9ShareFlags::ReadOnly, ReadOnly);
532 + hcs::AddPlan9Share(
533 + m_computeSystem.get(),
534 + shareName.c_str(),
535 + shareName.c_str(),
536 + WindowsPath,
537 + LX_INIT_UTILITY_VM_PLAN9_PORT,
538 + flags,
539 + m_userToken.get());
540 + }
541 + else
542 + {
543 + it->second = m_guestDeviceManager->AddGuestDevice(
544 + VIRTIO_FS_DEVICE_ID,
545 + m_virtioFsClassId,
546 + shareName.c_str(),
547 + ReadOnly ? L"ro" : L"",
548 + WindowsPath,
549 + VIRTIO_FS_FLAGS_TYPE_FILES,
550 + m_userToken.get());
551 + }
552 +
553 + cleanup.release();
554 +
555 + *ShareId = shareIdLocal;
556 + return S_OK;
557 +}
558 +CATCH_RETURN()
559 +
560 +HRESULT HcsVirtualMachine::RemoveShare(_In_ REFGUID ShareId)
561 +try
562 +{
563 + std::lock_guard lock(m_lock);
564 +
565 + auto it = m_shares.find(ShareId);
566 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_shares.end());
567 +
568 + if (!it->second.has_value())
569 + {
570 + auto shareName = wsl::shared::string::GuidToString<wchar_t>(it->first, wsl::shared::string::None);
571 + hcs::RemovePlan9Share(m_computeSystem.get(), shareName.c_str(), LX_INIT_UTILITY_VM_PLAN9_PORT);
572 + }
573 + else
574 + {
575 + m_guestDeviceManager->RemoveGuestDevice(VIRTIO_FS_DEVICE_ID, it->second.value());
576 + }
577 +
578 + m_shares.erase(it);
579 +
580 + return S_OK;
581 +}
582 +CATCH_RETURN()
583 +
584 +HRESULT HcsVirtualMachine::GetTerminationEvent(_Out_ HANDLE* Event)
585 +try
586 +{
587 + *Event = wslutil::DuplicateHandle(m_vmExitEvent.get());
588 +
589 + return S_OK;
590 +}
591 +CATCH_RETURN()
592 +
593 +void CALLBACK HcsVirtualMachine::OnVmExitCallback(HCS_EVENT* Event, void* Context)
594 +try
595 +{
596 + WSL_LOG(
597 + "OnVmExitCallback",
598 + TraceLoggingValue(Event->EventData, "details"),
599 + TraceLoggingValue(static_cast<int>(Event->Type), "type"));
600 +
601 + auto* vm = reinterpret_cast<HcsVirtualMachine*>(Context);
602 + if (Event->Type == HcsEventSystemExited)
603 + {
604 + vm->OnExit(Event);
605 + }
606 + else if (Event->Type == HcsEventSystemCrashInitiated || Event->Type == HcsEventSystemCrashReport)
607 + {
608 + vm->OnCrash(Event);
609 + }
610 +}
611 +CATCH_LOG()
612 +
613 +void HcsVirtualMachine::OnExit(const HCS_EVENT* Event)
614 +{
615 + m_vmExitEvent.SetEvent();
616 +
617 + const auto exitStatus = wsl::shared::FromJson<wsl::windows::common::hcs::SystemExitStatus>(Event->EventData);
618 +
619 + auto reason = WSLCVirtualMachineTerminationReasonUnknown;
620 +
621 + if (exitStatus.ExitType.has_value())
622 + {
623 + switch (exitStatus.ExitType.value())
624 + {
625 + case hcs::NotificationType::ForcedExit:
626 + case hcs::NotificationType::GracefulExit:
627 + reason = WSLCVirtualMachineTerminationReasonShutdown;
628 + break;
629 + case hcs::NotificationType::UnexpectedExit:
630 + reason = WSLCVirtualMachineTerminationReasonCrashed;
631 + break;
632 + default:
633 + reason = WSLCVirtualMachineTerminationReasonUnknown;
634 + break;
635 + }
636 + }
637 +
638 + if (m_terminationCallback)
639 + {
640 + LOG_IF_FAILED(m_terminationCallback->OnTermination(reason, Event->EventData));
641 + }
642 +}
643 +
644 +void HcsVirtualMachine::OnCrash(const HCS_EVENT* Event)
645 +{
646 + if (m_crashLogCaptured.load() && m_vmSavedStateCaptured.load())
647 + {
648 + return;
649 + }
650 +
651 + const auto crashReport = wsl::shared::FromJson<wsl::windows::common::hcs::CrashReport>(Event->EventData);
652 +
653 + if (crashReport.GuestCrashSaveInfo.has_value() && crashReport.GuestCrashSaveInfo->SaveStateFile.has_value())
654 + {
655 + if (!m_vmSavedStateCaptured.exchange(true))
656 + {
657 + auto resetFlag = wil::scope_exit([&]() noexcept { m_vmSavedStateCaptured.store(false); });
658 + EnforceVmSavedStateFileLimit();
659 + resetFlag.release();
660 + }
661 + }
662 +
663 + if (!crashReport.CrashLog.empty())
664 + {
665 + if (!m_crashLogCaptured.exchange(true))
666 + {
667 + auto resetFlag = wil::scope_exit([&]() noexcept { m_crashLogCaptured.store(false); });
668 + WriteCrashLog(crashReport.CrashLog);
669 + resetFlag.release();
670 + }
671 + }
672 +}
673 +
674 +std::filesystem::path HcsVirtualMachine::GetCrashDumpFolder()
675 +{
676 + auto tempPath = wsl::windows::common::filesystem::GetTempFolderPath(m_userToken.get());
677 + return tempPath / L"wslc-crashes";
678 +}
679 +
680 +void HcsVirtualMachine::CreateVmSavedStateFile(HANDLE InUserToken)
681 +{
682 + auto runAsUser = wil::impersonate_token(InUserToken);
683 +
684 + const auto filename = std::format(L"saved-state-{}-{}.vmrs", std::time(nullptr), m_vmIdString);
685 + auto savedStateFile = m_crashDumpFolder / filename;
686 +
687 + wsl::windows::common::filesystem::EnsureDirectory(m_crashDumpFolder.c_str());
688 +
689 + wil::unique_handle file{CreateFileW(savedStateFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, nullptr)};
690 + THROW_LAST_ERROR_IF(!file);
691 +
692 + hcs::GrantVmAccess(m_vmIdString.c_str(), savedStateFile.c_str());
693 + m_vmSavedStateFile = savedStateFile;
694 +}
695 +
696 +void HcsVirtualMachine::EnforceVmSavedStateFileLimit()
697 +{
698 + auto runAsUser = wil::impersonate_token(m_userToken.get());
699 +
700 + auto pred = [](const auto& e) {
701 + return WI_IsFlagSet(GetFileAttributes(e.path().c_str()), FILE_ATTRIBUTE_TEMPORARY) && e.path().has_extension() &&
702 + e.path().extension() == SAVED_STATE_FILE_EXTENSION && e.path().has_filename() &&
703 + e.path().filename().wstring().find(SAVED_STATE_FILE_PREFIX) == 0 && e.file_size() > 0;
704 + };
705 +
706 + wsl::windows::common::wslutil::EnforceFileLimit(m_crashDumpFolder.c_str(), MAX_VM_CRASH_FILES + 1, pred);
707 +}
708 +
709 +void HcsVirtualMachine::WriteCrashLog(const std::wstring& crashLog)
710 +{
711 + auto runAsUser = wil::impersonate_token(m_userToken.get());
712 +
713 + constexpr auto c_extension = L".txt";
714 + constexpr auto c_prefix = L"kernel-panic-";
715 + const auto filename = std::format(L"{}{}-{}{}", c_prefix, std::time(nullptr), m_vmIdString, c_extension);
716 + auto filePath = m_crashDumpFolder / filename;
717 +
718 + WI_ASSERT(std::filesystem::exists(m_crashDumpFolder));
719 + WI_ASSERT(std::filesystem::is_directory(m_crashDumpFolder));
720 +
721 + auto pred = [&c_extension, &c_prefix](const auto& e) {
722 + return WI_IsFlagSet(GetFileAttributes(e.path().c_str()), FILE_ATTRIBUTE_TEMPORARY) && e.path().has_extension() &&
723 + e.path().extension() == c_extension && e.path().has_filename() && e.path().filename().wstring().find(c_prefix) == 0;
724 + };
725 +
726 + wsl::windows::common::wslutil::EnforceFileLimit(m_crashDumpFolder.c_str(), MAX_VM_CRASH_FILES, pred);
727 +
728 + {
729 + std::wofstream outputFile(filePath.wstring());
730 + THROW_HR_IF(E_UNEXPECTED, !outputFile.is_open());
731 +
732 + outputFile << crashLog;
733 + THROW_HR_IF(E_UNEXPECTED, outputFile.fail());
734 + }
735 +
736 + THROW_IF_WIN32_BOOL_FALSE(SetFileAttributesW(filePath.c_str(), FILE_ATTRIBUTE_TEMPORARY));
737 +}
738 +
739 +ULONG HcsVirtualMachine::AllocateLun()
740 +{
741 + for (ULONG index = 0; index < gsl::narrow_cast<ULONG>(m_lunBitmap.size()); index += 1)
742 + {
743 + if (!m_lunBitmap[index])
744 + {
745 + m_lunBitmap[index] = true;
746 + return index;
747 + }
748 + }
749 +
750 + THROW_HR(WSL_E_TOO_MANY_DISKS_ATTACHED);
751 +}
752 +
753 +void HcsVirtualMachine::FreeLun(ULONG Lun)
754 +{
755 + THROW_HR_IF(E_BOUNDS, Lun >= m_lunBitmap.size());
756 + THROW_HR_IF(E_INVALIDARG, !m_lunBitmap[Lun]);
757 +
758 + m_lunBitmap[Lun] = false;
759 +}
\ No newline at end of file
src/windows/service/exe/HcsVirtualMachine.h new
+104
@@ -0,0 +1,104 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + HcsVirtualMachine.h
8 +
9 +Abstract:
10 +
11 + Implementation of IWSLCVirtualMachine - represents a single HCS-based VM instance.
12 + This class encapsulates a VM and all operations on it.
13 +
14 +--*/
15 +
16 +#pragma once
17 +
18 +#include <atomic>
19 +#include "wslc.h"
20 +#include "hcs.hpp"
21 +#include "GuestDeviceManager.h"
22 +#include "Dmesg.h"
23 +#include "INetworkingEngine.h"
24 +#include "WslCoreConfig.h"
25 +#include <filesystem>
26 +#include <map>
27 +
28 +#define MAX_VHD_COUNT 254
29 +
30 +namespace wsl::windows::service::wslc {
31 +
32 +class HcsVirtualMachine
33 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::WinRtClassicComMix>, IWSLCVirtualMachine, IFastRundown>
34 +{
35 +public:
36 + HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings);
37 + ~HcsVirtualMachine();
38 +
39 + // IWSLCVirtualMachine implementation
40 + IFACEMETHOD(GetId)(_Out_ GUID* VmId) override;
41 + IFACEMETHOD(AcceptConnection)(_Out_ HANDLE* Socket) override;
42 + IFACEMETHOD(ConfigureNetworking)(_In_ HANDLE GnsSocket, _In_opt_ HANDLE* DnsSocket) override;
43 + IFACEMETHOD(AttachDisk)(_In_ LPCWSTR Path, _In_ BOOL ReadOnly, _Out_ ULONG* Lun) override;
44 + IFACEMETHOD(DetachDisk)(_In_ ULONG Lun) override;
45 + IFACEMETHOD(AddShare)(_In_ LPCWSTR WindowsPath, _In_ BOOL ReadOnly, _Out_ GUID* ShareId) override;
46 + IFACEMETHOD(RemoveShare)(_In_ REFGUID ShareId) override;
47 + IFACEMETHOD(GetTerminationEvent)(_Out_ HANDLE* Event) override;
48 +
49 +private:
50 + struct DiskInfo
51 + {
52 + std::wstring Path;
53 + bool AccessGranted = false;
54 + };
55 +
56 + bool FeatureEnabled(WSLCFeatureFlags Value) const;
57 + static void CALLBACK OnVmExitCallback(HCS_EVENT* Event, void* Context);
58 + void OnExit(const HCS_EVENT* Event);
59 + void OnCrash(const HCS_EVENT* Event);
60 +
61 + std::filesystem::path GetCrashDumpFolder();
62 + void CreateVmSavedStateFile(HANDLE UserToken);
63 + void EnforceVmSavedStateFileLimit();
64 + void WriteCrashLog(const std::wstring& crashLog);
65 +
66 + ULONG AllocateLun();
67 + void FreeLun(ULONG Lun);
68 +
69 + std::recursive_mutex m_lock;
70 +
71 + wsl::windows::common::hcs::unique_hcs_system m_computeSystem;
72 + GUID m_vmId{};
73 + std::wstring m_vmIdString;
74 + ULONG m_bootTimeoutMs{};
75 +
76 + wil::shared_handle m_userToken;
77 + GUID m_virtioFsClassId{};
78 +
79 + WSLCFeatureFlags m_featureFlags{};
80 + WSLCNetworkingMode m_networkingMode{};
81 +
82 + wil::unique_socket m_listenSocket;
83 + std::shared_ptr<DmesgCollector> m_dmesgCollector;
84 + std::shared_ptr<GuestDeviceManager> m_guestDeviceManager;
85 + std::optional<wsl::core::Config> m_natConfig;
86 + std::unique_ptr<wsl::core::INetworkingEngine> m_networkEngine;
87 +
88 + wil::unique_event m_vmExitEvent{wil::EventOptions::ManualReset};
89 +
90 + std::map<ULONG, DiskInfo> m_attachedDisks;
91 + std::bitset<MAX_VHD_COUNT> m_lunBitmap;
92 +
93 + // Shares: key is ShareId, value is nullopt for Plan9 or DeviceInstanceId for VirtioFS
94 + std::map<GUID, std::optional<GUID>, wsl::windows::common::helpers::GuidLess> m_shares;
95 +
96 + std::filesystem::path m_vmSavedStateFile;
97 + std::filesystem::path m_crashDumpFolder;
98 + std::atomic<bool> m_vmSavedStateCaptured = false;
99 + std::atomic<bool> m_crashLogCaptured = false;
100 +
101 + wil::com_ptr<ITerminationCallback> m_terminationCallback;
102 +};
103 +
104 +} // namespace wsl::windows::service::wslc
src/windows/service/exe/LxssUserSession.cpp
+8 -6
@@ -3814,11 +3814,12 @@ void LxssUserSessionImpl::_ValidateDistributionNameAndPathNotInUse(
3814
3815 if (Name != nullptr && wsl::shared::string::IsEqual(Name, configuration.Name, true))
3816 {
3817 - THROW_HR_MSG(
3818 - (configuration.State == LxssDistributionStateInstalled) ? HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) : E_ILLEGAL_STATE_CHANGE,
3819 - "%ls already registered (state = %d)",
3820 - Name,
3821 - configuration.State);
3817 + THROW_HR_WITH_USER_ERROR_IF(
3818 + HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS),
3819 + wsl::shared::Localization::MessageDistroNameAlreadyExists(),
3820 + configuration.State == LxssDistributionStateInstalled);
3821 +
3822 + THROW_HR_MSG(E_ILLEGAL_STATE_CHANGE, "%ls already registered (state = %d)", Name, configuration.State);
3823 }
3824
3825 if (Path != nullptr)
@@ -3830,8 +3831,9 @@ void LxssUserSessionImpl::_ValidateDistributionNameAndPathNotInUse(
3831 }
3832
3833 // Ensure another distribution by a different name is not already registered to the same location.
3833 - THROW_HR_IF(
3834 + THROW_HR_WITH_USER_ERROR_IF(
3835 HRESULT_FROM_WIN32(ERROR_FILE_EXISTS),
3836 + wsl::shared::Localization::MessageDistroInstallPathAlreadyExists(),
3837 wsl::windows::common::string::IsPathComponentEqual(error ? configuration.BasePath.native() : canonicalDistroPath.native(), Path));
3838 }
3839 }
src/windows/service/exe/LxssUserSessionFactory.cpp
-1
@@ -35,7 +35,6 @@ std::optional<wsl::windows::service::PluginManager> g_pluginManager;
35 extern unique_event g_networkingReady;
36 extern bool g_lxcoreInitialized;
37
38 -_Requires_lock_held_(g_sessionLock)
38 void ClearSessionsAndBlockNewInstancesLockHeld(std::optional<std::vector<std::shared_ptr<LxssUserSessionImpl>>>& sessions)
39 {
40 std::lock_guard lock(g_sessionTerminationLock);
src/windows/service/exe/ServiceMain.cpp
+10 -3
@@ -18,6 +18,7 @@ Abstract:
18 #include "WslCoreFilesystem.h"
19 #include "LxssIpTables.h"
20 #include "LxssUserSessionFactory.h"
21 +#include "WSLCSessionManagerFactory.h"
22 #include <ctime>
23
24 using namespace wsl::windows::common::registry;
@@ -31,6 +32,9 @@ wil::unique_event g_networkingReady{wil::EventOptions::ManualReset};
32 // Declare the LxssUserSession COM class.
33 CoCreatableClassWrlCreatorMapInclude(LxssUserSession);
34
35 +// Declare the WSLCSessionManager COM class.
36 +CoCreatableClassWrlCreatorMapInclude(WSLCSessionManager);
37 +
38 struct WslServiceSecurityPolicy
39 {
40 static LPCWSTR GetSDDLText()
@@ -170,9 +174,6 @@ try
174
175 wsl::windows::common::security::ApplyProcessMitigationPolicies();
176
173 - // Ensure that the OS has support for running lifted WSL.
174 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_SERVICE_DISABLED), !wsl::windows::common::helpers::IsWslSupportInterfacePresent());
175 -
177 // Initialize Winsock.
178 WSADATA Data;
179 THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &Data));
@@ -189,6 +190,9 @@ try
190 });
191
192 EvaluateWslPolicy();
193 +
194 + wsl::windows::common::helpers::RegisterWithDcat();
195 +
196 return S_OK;
197 }
198 CATCH_RETURN()
@@ -240,6 +244,9 @@ void WslService::ServiceStopped()
244 // Terminate all user sessions.
245 ClearSessionsAndBlockNewInstances();
246
247 + // Also tear down WSLC sessions.
248 + wsl::windows::service::wslc::ClearWslcSessionsAndBlockNewInstances();
249 +
250 // Disconnect from the LxCore driver.
251 if (g_lxcoreInitialized)
252 {
src/windows/service/exe/WSLCSessionManager.cpp new
+466
@@ -0,0 +1,466 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionManager.cpp
8 +
9 +Abstract:
10 +
11 + Implementation for WSLCSessionManager.
12 +
13 + Sessions run in a per-user COM server process for security isolation.
14 + The SYSTEM service creates sessions via IWSLCSessionFactory which returns
15 + both the session interface (for clients) and an IWSLCSessionReference
16 + (for the service to track sessions via weak references).
17 +
18 + Session lifetime:
19 + - Non-persistent sessions: tracked via IWSLCSessionReference which holds
20 + weak references. Sessions are cleaned up when all client refs are released.
21 + - Persistent sessions: the service holds an additional strong IWSLCSession
22 + reference to keep them alive until explicitly terminated.
23 +
24 + A job object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ensures that all
25 + per-user COM server processes are automatically terminated if wslservice
26 + crashes or exits unexpectedly.
27 +
28 +--*/
29 +
30 +#include "WSLCSessionManager.h"
31 +#include "HcsVirtualMachine.h"
32 +#include "WSLCUserSettings.h"
33 +#include "WSLCSessionDefaults.h"
34 +#include "wslutil.h"
35 +#include "filesystem.hpp"
36 +
37 +using wsl::windows::service::wslc::CallingProcessTokenInfo;
38 +using wsl::windows::service::wslc::HcsVirtualMachine;
39 +using wsl::windows::service::wslc::WSLCSessionManagerImpl;
40 +namespace wslutil = wsl::windows::common::wslutil;
41 +namespace settings = wsl::windows::wslc::settings;
42 +
43 +namespace {
44 +
45 +// Session settings built server-side from the caller's settings.yaml.
46 +struct SessionSettings
47 +{
48 + std::wstring DisplayName;
49 + std::wstring StoragePath;
50 + WSLCSessionSettings Settings{};
51 +
52 + NON_COPYABLE(SessionSettings);
53 + NON_MOVABLE(SessionSettings);
54 +
55 + // Load user settings under impersonation.
56 + static settings::UserSettings LoadUserSettings(HANDLE UserToken)
57 + {
58 + auto localAppData = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken);
59 + auto runAsUser = wil::impersonate_token(UserToken);
60 + return settings::UserSettings(localAppData / L"wslc");
61 + }
62 +
63 + // Get default memory size. Half of available memory.
64 + static uint32_t DefaultMemoryMb()
65 + {
66 + MEMORYSTATUSEX memInfo{sizeof(MEMORYSTATUSEX)};
67 + THROW_IF_WIN32_BOOL_FALSE(GlobalMemoryStatusEx(&memInfo));
68 + return static_cast<uint32_t>(memInfo.ullTotalPhys / (2 * _1MB));
69 + }
70 +
71 + // Default session: name and storage path determined from caller's token.
72 + static std::unique_ptr<SessionSettings> Default(HANDLE UserToken, const std::wstring& ResolvedName)
73 + {
74 + auto userSettings = LoadUserSettings(UserToken);
75 + auto localAppData = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken);
76 +
77 + auto storagePath = (localAppData / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName).wstring();
78 +
79 + return std::unique_ptr<SessionSettings>(
80 + new SessionSettings(std::wstring(ResolvedName), std::move(storagePath), WSLCSessionStorageFlagsNone, userSettings));
81 + }
82 +
83 + // Custom session: caller provides name and storage path.
84 + static SessionSettings Custom(HANDLE UserToken, LPCWSTR Name, LPCWSTR Path, WSLCSessionStorageFlags StorageFlags = WSLCSessionStorageFlagsNone)
85 + {
86 + auto userSettings = LoadUserSettings(UserToken);
87 + return SessionSettings(Name, Path, StorageFlags, userSettings);
88 + }
89 +
90 +private:
91 + SessionSettings(std::wstring name, std::wstring path, WSLCSessionStorageFlags storageFlags, const settings::UserSettings& userSettings) :
92 + DisplayName(std::move(name)), StoragePath(std::move(path))
93 + {
94 + Settings.DisplayName = DisplayName.c_str();
95 + Settings.StoragePath = StoragePath.c_str();
96 + auto cpuCount = userSettings.Get<settings::Setting::SessionCpuCount>();
97 + Settings.CpuCount = cpuCount > 0 ? cpuCount : wsl::windows::common::wslutil::GetLogicalProcessorCount();
98 + auto memoryMb = userSettings.Get<settings::Setting::SessionMemoryMb>();
99 + Settings.MemoryMb = memoryMb > 0 ? memoryMb : SessionSettings::DefaultMemoryMb();
100 + Settings.MaximumStorageSizeMb = userSettings.Get<settings::Setting::SessionStorageSizeMb>();
101 + Settings.BootTimeoutMs = wsl::windows::wslc::DefaultBootTimeoutMs;
102 + Settings.NetworkingMode = userSettings.Get<settings::Setting::SessionNetworkingMode>();
103 + Settings.FeatureFlags = WslcFeatureFlagsNone;
104 + WI_SetFlagIf(Settings.FeatureFlags, WslcFeatureFlagsDnsTunneling, userSettings.Get<settings::Setting::SessionDnsTunneling>());
105 + WI_SetFlagIf(
106 + Settings.FeatureFlags,
107 + WslcFeatureFlagsVirtioFs,
108 + userSettings.Get<settings::Setting::SessionHostFileShareMode>() == settings::HostFileShareMode::VirtioFs);
109 + Settings.StorageFlags = storageFlags;
110 + }
111 +};
112 +
113 +} // namespace
114 +
115 +WSLCSessionManagerImpl::~WSLCSessionManagerImpl()
116 +{
117 + // Terminate all sessions on shutdown.
118 + // Call Terminate() directly rather than going through ForEachSession(),
119 + // which would needlessly resolve weak references and call GetState().
120 + // Terminate() already handles the "session is gone" case gracefully.
121 + std::lock_guard lock(m_wslcSessionsLock);
122 + for (auto& entry : m_sessions)
123 + {
124 + LOG_IF_FAILED(entry.Ref->Terminate());
125 + }
126 +}
127 +
128 +void WSLCSessionManagerImpl::CreateSession(const WSLCSessionSettings* Settings, WSLCSessionFlags Flags, IWSLCSession** WslcSession)
129 +{
130 + auto tokenInfo = GetCallingProcessTokenInfo();
131 + const auto callerToken = wsl::windows::common::security::GetUserToken(TokenImpersonation);
132 +
133 + // Resolve display name upfront (for both default and custom sessions).
134 + std::wstring resolvedDisplayName;
135 + if (Settings == nullptr)
136 + {
137 + // Default session: name determined from token, qualified with username.
138 + resolvedDisplayName = ResolveDefaultSessionName(tokenInfo);
139 + Flags = WSLCSessionFlagsOpenExisting | WSLCSessionFlagsPersistent;
140 + }
141 + else
142 + {
143 + THROW_HR_IF(WSLC_E_INVALID_SESSION_NAME, Settings->DisplayName == nullptr || wcslen(Settings->DisplayName) == 0);
144 + THROW_HR_IF(E_INVALIDARG, Settings->StoragePath != nullptr && wcslen(Settings->StoragePath) == 0);
145 + THROW_HR_IF(WSLC_E_INVALID_SESSION_NAME, wcslen(Settings->DisplayName) >= std::size(WSLCSessionInformation{}.DisplayName));
146 + THROW_HR_IF_MSG(
147 + E_INVALIDARG,
148 + WI_IsAnyFlagSet(Settings->StorageFlags, ~WSLCSessionStorageFlagsValid),
149 + "Invalid storage flags: %i",
150 + Settings->StorageFlags);
151 +
152 + // Reserved names can only be assigned server-side via null Settings.
153 + THROW_HR_IF(WSLC_E_SESSION_RESERVED, IsReservedSessionName(Settings->DisplayName));
154 +
155 + resolvedDisplayName = Settings->DisplayName;
156 + }
157 +
158 + std::lock_guard lock(m_wslcSessionsLock);
159 +
160 + // Check for an existing session first.
161 + auto result = ForEachSession<HRESULT>([&](auto& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> {
162 + if (!wsl::shared::string::IsEqual(entry.DisplayName.c_str(), resolvedDisplayName.c_str()))
163 + {
164 + return {};
165 + }
166 +
167 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), WI_IsFlagClear(Flags, WSLCSessionFlagsOpenExisting));
168 +
169 + RETURN_IF_FAILED(CheckTokenAccess(entry, tokenInfo));
170 +
171 + RETURN_IF_FAILED(wil::com_copy_to_nothrow(session, WslcSession));
172 +
173 + return S_OK;
174 + });
175 +
176 + if (result.has_value())
177 + {
178 + THROW_IF_FAILED(result.value());
179 + return; // Existing session was opened.
180 + }
181 +
182 + wslutil::StopWatch stopWatch;
183 +
184 + // Initialize settings for the default session.
185 + std::unique_ptr<SessionSettings> defaultSettings;
186 + if (Settings == nullptr)
187 + {
188 + defaultSettings = SessionSettings::Default(callerToken.get(), resolvedDisplayName);
189 + Settings = &defaultSettings->Settings;
190 + }
191 +
192 + HRESULT creationResult = wil::ResultFromException([&]() {
193 + // Get caller info.
194 + const auto callerProcess = wslutil::OpenCallingProcess(PROCESS_QUERY_LIMITED_INFORMATION);
195 + const ULONG sessionId = m_nextSessionId++;
196 + const DWORD creatorPid = GetProcessId(callerProcess.get());
197 + const auto userToken = wsl::windows::common::security::GetUserToken(TokenImpersonation);
198 +
199 + // Create the VM in the SYSTEM service (privileged).
200 + auto vm = Microsoft::WRL::Make<HcsVirtualMachine>(Settings);
201 +
202 + // Launch per-user COM server factory and add it to our job object for crash cleanup.
203 + auto factory = wslutil::CreateComServerAsUser<IWSLCSessionFactory>(__uuidof(WSLCSessionFactory), userToken.get());
204 + AddSessionProcessToJobObject(factory.get());
205 +
206 + // Create the session via the factory.
207 + const auto sessionSettings = CreateSessionSettings(sessionId, creatorPid, Settings, resolvedDisplayName.c_str());
208 + wil::com_ptr<IWSLCSession> session;
209 + wil::com_ptr<IWSLCSessionReference> serviceRef;
210 + THROW_IF_FAILED(factory->CreateSession(&sessionSettings, vm.Get(), &session, &serviceRef));
211 +
212 + // Track the session via its service ref, along with metadata and security info.
213 + m_sessions.push_back({std::move(serviceRef), sessionId, creatorPid, resolvedDisplayName, std::move(tokenInfo)});
214 +
215 + // For persistent sessions, also hold a strong reference to keep them alive.
216 + const bool persistent = WI_IsFlagSet(Flags, WSLCSessionFlagsPersistent);
217 + if (persistent)
218 + {
219 + m_persistentSessions.emplace_back(sessionId, session);
220 + }
221 +
222 + *WslcSession = session.detach();
223 + });
224 +
225 + // This telemetry event is used to keep track of session creation performance (via CreationTimeMs) and failure reasons (via Result).
226 + WSL_LOG_TELEMETRY(
227 + "WSLCCreateSession",
228 + PDT_ProductAndServicePerformance,
229 + TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA),
230 + TraceLoggingValue(resolvedDisplayName.c_str(), "Name"),
231 + TraceLoggingValue(stopWatch.ElapsedMilliseconds(), "CreationTimeMs"),
232 + TraceLoggingValue(creationResult, "Result"),
233 + TraceLoggingValue(tokenInfo.Elevated, "Elevated"),
234 + TraceLoggingValue(static_cast<uint32_t>(Flags), "Flags"),
235 + TraceLoggingLevel(WINEVENT_LEVEL_INFO));
236 +
237 + THROW_IF_FAILED_MSG(creationResult, "Failed to create session: %ls", resolvedDisplayName.c_str());
238 +}
239 +
240 +void WSLCSessionManagerImpl::OpenSession(ULONG Id, IWSLCSession** Session)
241 +{
242 + auto tokenInfo = GetCallingProcessTokenInfo();
243 + auto result = ForEachSession<HRESULT>([&](auto& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> {
244 + if (entry.SessionId != Id)
245 + {
246 + return {};
247 + }
248 +
249 + RETURN_IF_FAILED(CheckTokenAccess(entry, tokenInfo));
250 +
251 + RETURN_IF_FAILED(wil::com_copy_to_nothrow(session, Session));
252 +
253 + return S_OK;
254 + });
255 +
256 + THROW_IF_FAILED_MSG(result.value_or(HRESULT_FROM_WIN32(ERROR_NOT_FOUND)), "Session '%lu' not found", Id);
257 +}
258 +
259 +void WSLCSessionManagerImpl::OpenSessionByName(LPCWSTR DisplayName, IWSLCSession** Session)
260 +{
261 + auto tokenInfo = GetCallingProcessTokenInfo();
262 +
263 + // Null name = default session, resolved from caller's token + username.
264 + std::wstring resolvedName;
265 + if (DisplayName == nullptr)
266 + {
267 + resolvedName = ResolveDefaultSessionName(tokenInfo);
268 + DisplayName = resolvedName.c_str();
269 + }
270 +
271 + auto result = ForEachSession<HRESULT>([&](auto& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> {
272 + if (!wsl::shared::string::IsEqual(entry.DisplayName.c_str(), DisplayName))
273 + {
274 + return {};
275 + }
276 +
277 + RETURN_IF_FAILED(CheckTokenAccess(entry, tokenInfo));
278 +
279 + RETURN_IF_FAILED(wil::com_copy_to_nothrow(session, Session));
280 +
281 + return S_OK;
282 + });
283 +
284 + THROW_IF_FAILED_MSG(result.value_or(HRESULT_FROM_WIN32(ERROR_NOT_FOUND)), "Session '%ls' not found", DisplayName);
285 +}
286 +
287 +void WSLCSessionManagerImpl::ListSessions(_Out_ WSLCSessionInformation** Sessions, _Out_ ULONG* SessionsCount)
288 +{
289 + std::vector<WSLCSessionInformation> sessionInfo;
290 +
291 + ForEachSession<void>([&](auto& entry, const auto&) noexcept {
292 + try
293 + {
294 + wil::unique_hlocal_string sidString;
295 + THROW_IF_WIN32_BOOL_FALSE(ConvertSidToStringSidW(entry.Owner.TokenInfo->User.Sid, &sidString));
296 +
297 + auto& it = sessionInfo.emplace_back(WSLCSessionInformation{.SessionId = entry.SessionId, .CreatorPid = entry.CreatorPid});
298 + wcscpy_s(it.Sid, _countof(it.Sid), sidString.get());
299 + wcscpy_s(it.DisplayName, _countof(it.DisplayName), entry.DisplayName.c_str());
300 + }
301 + CATCH_LOG()
302 + });
303 +
304 + auto output = wil::make_unique_cotaskmem<WSLCSessionInformation[]>(sessionInfo.size());
305 + memcpy(output.get(), sessionInfo.data(), sessionInfo.size() * sizeof(WSLCSessionInformation));
306 +
307 + *Sessions = output.release();
308 + *SessionsCount = static_cast<ULONG>(sessionInfo.size());
309 +}
310 +
311 +void WSLCSessionManagerImpl::GetVersion(_Out_ WSLCVersion* Version)
312 +{
313 + Version->Major = WSL_PACKAGE_VERSION_MAJOR;
314 + Version->Minor = WSL_PACKAGE_VERSION_MINOR;
315 + Version->Revision = WSL_PACKAGE_VERSION_REVISION;
316 +}
317 +
318 +void WSLCSessionManagerImpl::EnterSession(_In_ LPCWSTR DisplayName, _In_ LPCWSTR StoragePath, IWSLCSession** WslcSession)
319 +{
320 + THROW_HR_IF(E_POINTER, DisplayName == nullptr || StoragePath == nullptr);
321 + THROW_HR_IF(E_INVALIDARG, DisplayName[0] == L'\0' || StoragePath[0] == L'\0');
322 +
323 + const auto callerToken = wsl::windows::common::security::GetUserToken(TokenImpersonation);
324 + auto sessionSettings = SessionSettings::Custom(callerToken.get(), DisplayName, StoragePath, WSLCSessionStorageFlagsNoCreate);
325 + CreateSession(&sessionSettings.Settings, WSLCSessionFlagsNone, WslcSession);
326 +}
327 +
328 +WSLCSessionInitSettings WSLCSessionManagerImpl::CreateSessionSettings(
329 + _In_ ULONG SessionId, _In_ DWORD CreatorPid, _In_ const WSLCSessionSettings* Settings, _In_ LPCWSTR ResolvedDisplayName)
330 +{
331 + WSLCSessionInitSettings sessionSettings{};
332 + sessionSettings.SessionId = SessionId;
333 + sessionSettings.CreatorPid = CreatorPid;
334 + sessionSettings.DisplayName = ResolvedDisplayName;
335 + sessionSettings.StoragePath = Settings->StoragePath;
336 + sessionSettings.MaximumStorageSizeMb = Settings->MaximumStorageSizeMb;
337 + sessionSettings.BootTimeoutMs = Settings->BootTimeoutMs;
338 + sessionSettings.NetworkingMode = Settings->NetworkingMode;
339 + sessionSettings.FeatureFlags = Settings->FeatureFlags;
340 + sessionSettings.RootVhdTypeOverride = Settings->RootVhdTypeOverride;
341 + sessionSettings.StorageFlags = Settings->StorageFlags;
342 + return sessionSettings;
343 +}
344 +
345 +void WSLCSessionManagerImpl::AddSessionProcessToJobObject(_In_ IWSLCSessionFactory* Factory)
346 +{
347 + EnsureJobObjectCreated();
348 +
349 + wil::unique_handle process;
350 + THROW_IF_FAILED(Factory->GetProcessHandle(process.put()));
351 +
352 + THROW_IF_WIN32_BOOL_FALSE(AssignProcessToJobObject(m_sessionJobObject.get(), process.get()));
353 +}
354 +
355 +void WSLCSessionManagerImpl::EnsureJobObjectCreated()
356 +{
357 + // Create a job object that will automatically terminate all child processes
358 + // when the job handle is closed (i.e., when wslservice exits or crashes).
359 + std::call_once(m_jobObjectInitFlag, [this] {
360 + m_sessionJobObject.reset(CreateJobObjectW(nullptr, nullptr));
361 + THROW_LAST_ERROR_IF(!m_sessionJobObject);
362 +
363 + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo{};
364 + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
365 + THROW_IF_WIN32_BOOL_FALSE(
366 + SetInformationJobObject(m_sessionJobObject.get(), JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo)));
367 +
368 + WSL_LOG("SessionManagerJobObjectCreated", TraceLoggingLevel(WINEVENT_LEVEL_INFO));
369 + });
370 +}
371 +
372 +CallingProcessTokenInfo WSLCSessionManagerImpl::GetCallingProcessTokenInfo()
373 +{
374 + const wil::unique_handle userToken = wsl::windows::common::security::GetUserToken(TokenImpersonation);
375 +
376 + auto tokenInfo = wil::get_token_information<TOKEN_USER>(userToken.get());
377 + auto elevated = wil::test_token_membership(userToken.get(), SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS);
378 +
379 + return {std::move(tokenInfo), elevated};
380 +}
381 +
382 +std::wstring WSLCSessionManagerImpl::ResolveDefaultSessionName(const CallingProcessTokenInfo& TokenInfo)
383 +{
384 + // Look up the username from the caller's SID so each user gets their own
385 + // default session (e.g. "wslc-cli-alice", "wslc-cli-admin-bob").
386 + wchar_t username[256 + 1] = {};
387 + DWORD usernameLen = ARRAYSIZE(username);
388 + wchar_t domain[MAX_PATH] = {};
389 + DWORD domainLen = ARRAYSIZE(domain);
390 + SID_NAME_USE sidType;
391 + THROW_IF_WIN32_BOOL_FALSE(LookupAccountSidW(nullptr, TokenInfo.TokenInfo->User.Sid, username, &usernameLen, domain, &domainLen, &sidType));
392 +
393 + auto baseName = TokenInfo.Elevated ? wsl::windows::wslc::DefaultAdminSessionName : wsl::windows::wslc::DefaultSessionName;
394 + return std::format(L"{}-{}", baseName, username);
395 +}
396 +
397 +bool WSLCSessionManagerImpl::IsReservedSessionName(LPCWSTR Name)
398 +{
399 + // Block any name that is exactly "wslc-cli" or starts with "wslc-cli-",
400 + // which covers the admin variant and all per-user resolved names.
401 + constexpr std::wstring_view prefix{wsl::windows::wslc::DefaultSessionName};
402 + std::wstring_view name{Name};
403 + if (name.size() < prefix.size())
404 + {
405 + return false;
406 + }
407 +
408 + if (!wsl::shared::string::IsEqual(name.substr(0, prefix.size()), prefix, true))
409 + {
410 + return false;
411 + }
412 +
413 + return name.size() == prefix.size() || name[prefix.size()] == L'-';
414 +}
415 +
416 +HRESULT WSLCSessionManagerImpl::CheckTokenAccess(const SessionEntry& Entry, const CallingProcessTokenInfo& TokenInfo)
417 +{
418 + // Allow elevated tokens to access all sessions.
419 + // Otherwise a token can only access sessions from the same SID and elevation status.
420 + // TODO: Offer proper ACL checks.
421 +
422 + if (TokenInfo.Elevated)
423 + {
424 + return S_OK; // Token is elevated, allow access.
425 + }
426 +
427 + RETURN_HR_IF(E_ACCESSDENIED, !EqualSid(Entry.Owner.TokenInfo->User.Sid, TokenInfo.TokenInfo->User.Sid)); // Different account, deny access.
428 +
429 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ELEVATION_REQUIRED), Entry.Owner.Elevated); // Non-elevated token trying to access elevated session, deny access.
430 +
431 + return S_OK;
432 +}
433 +
434 +WSLCSessionManager::WSLCSessionManager(WSLCSessionManagerImpl* Impl) : COMImplClass<WSLCSessionManagerImpl>(Impl)
435 +{
436 +}
437 +
438 +HRESULT WSLCSessionManager::GetVersion(_Out_ WSLCVersion* Version)
439 +{
440 + return CallImpl(&WSLCSessionManagerImpl::GetVersion, Version);
441 +}
442 +
443 +HRESULT WSLCSessionManager::CreateSession(const WSLCSessionSettings* WslcSessionSettings, WSLCSessionFlags Flags, IWSLCSession** WslcSession)
444 +{
445 + return CallImpl(&WSLCSessionManagerImpl::CreateSession, WslcSessionSettings, Flags, WslcSession);
446 +}
447 +
448 +HRESULT WSLCSessionManager::EnterSession(_In_ LPCWSTR DisplayName, _In_ LPCWSTR StoragePath, IWSLCSession** WslcSession)
449 +{
450 + return CallImpl(&WSLCSessionManagerImpl::EnterSession, DisplayName, StoragePath, WslcSession);
451 +}
452 +
453 +HRESULT WSLCSessionManager::ListSessions(_Out_ WSLCSessionInformation** Sessions, _Out_ ULONG* SessionsCount)
454 +{
455 + return CallImpl(&WSLCSessionManagerImpl::ListSessions, Sessions, SessionsCount);
456 +}
457 +
458 +HRESULT WSLCSessionManager::OpenSession(_In_ ULONG Id, _Out_ IWSLCSession** Session)
459 +{
460 + return CallImpl(&WSLCSessionManagerImpl::OpenSession, Id, Session);
461 +}
462 +
463 +HRESULT WSLCSessionManager::OpenSessionByName(_In_ LPCWSTR DisplayName, _Out_ IWSLCSession** Session)
464 +{
465 + return CallImpl(&WSLCSessionManagerImpl::OpenSessionByName, DisplayName, Session);
466 +}
src/windows/service/exe/WSLCSessionManager.h new
+189
@@ -0,0 +1,189 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionManager.h
8 +
9 +Abstract:
10 +
11 + Definition for WSLCSessionManager.
12 +
13 + Session Lifetime Management:
14 + ----------------------------
15 + Sessions are created in per-user COM server processes via IWSLCSessionFactory.
16 + The SYSTEM service holds IWSLCSessionReference objects that contain weak
17 + references to the actual sessions.
18 +
19 + - Non-persistent sessions: Lifetime is tied to client COM references.
20 + When all clients release their IWSLCSession references, the session is
21 + terminated and the weak reference in IWSLCSessionReference returns NULL.
22 +
23 + - Persistent sessions: The service holds an additional strong IWSLCSession
24 + reference to keep the session alive until explicitly terminated or service
25 + shutdown.
26 +
27 + The IWSLCSessionReference allows the service to:
28 + - Check if a session is still alive (OpenSession fails if session is gone)
29 + - Terminate sessions when requested by elevated callers
30 +
31 +--*/
32 +
33 +#pragma once
34 +#include "wslc.h"
35 +#include "COMImplClass.h"
36 +#include "wslutil.h"
37 +#include <atomic>
38 +#include <algorithm>
39 +#include <string>
40 +#include <vector>
41 +#include <mutex>
42 +#include <type_traits>
43 +
44 +namespace wslutil = wsl::windows::common::wslutil;
45 +
46 +namespace wsl::windows::service::wslc {
47 +
48 +struct CallingProcessTokenInfo
49 +{
50 + wil::unique_tokeninfo_ptr<TOKEN_USER> TokenInfo;
51 + bool Elevated;
52 +};
53 +
54 +// Metadata for a tracked session, stored service-side at creation time.
55 +// Security info is stored here (not queried from the per-user process) to prevent spoofing.
56 +struct SessionEntry
57 +{
58 + wil::com_ptr<IWSLCSessionReference> Ref;
59 + ULONG SessionId = 0;
60 + DWORD CreatorPid = 0;
61 + std::wstring DisplayName;
62 + CallingProcessTokenInfo Owner;
63 +};
64 +
65 +class WSLCSessionManagerImpl
66 +{
67 +public:
68 + NON_COPYABLE(WSLCSessionManagerImpl);
69 + NON_MOVABLE(WSLCSessionManagerImpl);
70 +
71 + WSLCSessionManagerImpl() = default;
72 + ~WSLCSessionManagerImpl();
73 +
74 + void GetVersion(_Out_ WSLCVersion* Version);
75 + void CreateSession(const WSLCSessionSettings* WslcSessionSettings, WSLCSessionFlags Flags, IWSLCSession** WslcSession);
76 + void EnterSession(_In_ LPCWSTR DisplayName, _In_ LPCWSTR StoragePath, IWSLCSession** WslcSession);
77 + void ListSessions(_Out_ WSLCSessionInformation** Sessions, _Out_ ULONG* SessionsCount);
78 + void OpenSession(_In_ ULONG Id, _Out_ IWSLCSession** Session);
79 + void OpenSessionByName(_In_ LPCWSTR DisplayName, _Out_ IWSLCSession** Session);
80 +
81 +private:
82 + // Resolves the default session name for a caller: appends the username
83 + // from the token SID so different users don't collide.
84 + static std::wstring ResolveDefaultSessionName(const CallingProcessTokenInfo& TokenInfo);
85 +
86 + // Returns true if the name matches a reserved default session prefix.
87 + static bool IsReservedSessionName(LPCWSTR Name);
88 +
89 + // Iterates over all sessions, cleaning up released sessions.
90 + // The routine receives a SessionEntry& and can return an optional<T> to stop iteration.
91 + template <typename T>
92 + inline auto ForEachSession(const auto& Routine)
93 + {
94 + std::lock_guard lock(m_wslcSessionsLock);
95 +
96 + // Enforce noexcept: remove_if leaves the container in an unspecified
97 + // (partially-moved) state if the predicate throws. Callers must handle
98 + // errors via return values, not exceptions.
99 + static_assert(
100 + std::is_nothrow_invocable_v<decltype(Routine), SessionEntry&, wil::com_ptr<IWSLCSession>&>,
101 + "ForEachSession routine must be noexcept to preserve container invariants during remove_if");
102 +
103 + using TResult = std::conditional_t<std::is_same_v<T, void>, nullptr_t, std::optional<T>>;
104 + TResult result{};
105 +
106 + auto each = [&](SessionEntry& entry) {
107 + // Try to open the session via the service ref.
108 + // Fails with ERROR_OBJECT_NO_LONGER_EXISTS if released,
109 + // ERROR_INVALID_STATE if terminated, or RPC error if per-user process is dead.
110 + wil::com_ptr<IWSLCSession> lockedSession;
111 + if (FAILED_LOG(entry.Ref->OpenSession(&lockedSession)))
112 + {
113 + // Session is gone, drop the persistent reference if any.
114 + auto remove =
115 + std::ranges::remove_if(m_persistentSessions, [&](const auto& e) { return e.first == entry.SessionId; });
116 + m_persistentSessions.erase(remove.begin(), remove.end());
117 + return true; // Remove from tracking
118 + }
119 +
120 + if constexpr (std::is_same_v<T, void>)
121 + {
122 + Routine(entry, lockedSession);
123 + }
124 + else
125 + {
126 + if (!result.has_value())
127 + {
128 + result = Routine(entry, lockedSession);
129 + }
130 + }
131 +
132 + return false; // Keep in tracking
133 + };
134 +
135 + auto remove = std::ranges::remove_if(m_sessions, each);
136 + m_sessions.erase(remove.begin(), remove.end());
137 +
138 + if constexpr (std::is_same_v<T, void>)
139 + {
140 + return;
141 + }
142 + else
143 + {
144 + return result;
145 + }
146 + }
147 +
148 + void AddSessionProcessToJobObject(_In_ IWSLCSessionFactory* Factory);
149 + WSLCSessionInitSettings CreateSessionSettings(
150 + _In_ ULONG SessionId, _In_ DWORD CreatorPid, _In_ const WSLCSessionSettings* Settings, _In_ LPCWSTR ResolvedDisplayName);
151 + void EnsureJobObjectCreated();
152 + static CallingProcessTokenInfo GetCallingProcessTokenInfo();
153 + static HRESULT CheckTokenAccess(const SessionEntry& Entry, const CallingProcessTokenInfo& TokenInfo);
154 +
155 + std::atomic<ULONG> m_nextSessionId{1};
156 + std::recursive_mutex m_wslcSessionsLock;
157 +
158 + // Job object that automatically terminates all child COM server processes
159 + // when this service exits or crashes (JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE).
160 + std::once_flag m_jobObjectInitFlag;
161 + wil::unique_handle m_sessionJobObject;
162 +
163 + // All sessions tracked via SessionEntry (which holds weak refs and service-side security info).
164 + // Sessions are automatically cleaned up when the underlying session is released.
165 + std::vector<SessionEntry> m_sessions;
166 +
167 + // Strong references to persistent sessions to keep them alive.
168 + // Session ID is stored alongside so cleanup doesn't require cross-process COM calls.
169 + std::vector<std::pair<ULONG, wil::com_ptr<IWSLCSession>>> m_persistentSessions;
170 +};
171 +} // namespace wsl::windows::service::wslc
172 +
173 +class DECLSPEC_UUID("a9b7a1b9-0671-405c-95f1-e0612cb4ce8f") WSLCSessionManager
174 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCSessionManager, IFastRundown>,
175 + public wsl::windows::service::wslc::COMImplClass<wsl::windows::service::wslc::WSLCSessionManagerImpl>
176 +{
177 +public:
178 + NON_COPYABLE(WSLCSessionManager);
179 + NON_MOVABLE(WSLCSessionManager);
180 +
181 + WSLCSessionManager(wsl::windows::service::wslc::WSLCSessionManagerImpl* Impl);
182 +
183 + IFACEMETHOD(GetVersion)(_Out_ WSLCVersion* Version) override;
184 + IFACEMETHOD(CreateSession)(const WSLCSessionSettings* WslcSessionSettings, WSLCSessionFlags Flags, IWSLCSession** WslcSession) override;
185 + IFACEMETHOD(EnterSession)(_In_ LPCWSTR DisplayName, _In_ LPCWSTR StoragePath, IWSLCSession** WslcSession) override;
186 + IFACEMETHOD(ListSessions)(_Out_ WSLCSessionInformation** Sessions, _Out_ ULONG* SessionsCount) override;
187 + IFACEMETHOD(OpenSession)(_In_ ULONG Id, _Out_ IWSLCSession** Session) override;
188 + IFACEMETHOD(OpenSessionByName)(_In_ LPCWSTR DisplayName, _Out_ IWSLCSession** Session) override;
189 +};
src/windows/service/exe/WSLCSessionManagerFactory.cpp new
+76
@@ -0,0 +1,76 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionManagerFactory.cpp
8 +
9 +Abstract:
10 +
11 + Contains the implementation for WSLCSessionManagerFactory.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +
17 +#include "WSLCSessionManagerFactory.h"
18 +#include "WSLCSessionManager.h"
19 +
20 +using wsl::windows::service::wslc::WSLCSessionManagerFactory;
21 +using wsl::windows::service::wslc::WSLCSessionManagerImpl;
22 +
23 +CoCreatableClassWithFactory(WSLCSessionManager, WSLCSessionManagerFactory);
24 +
25 +static std::mutex g_mutex;
26 +static std::optional<WSLCSessionManagerImpl> g_sessionManagerImpl = std::make_optional<WSLCSessionManagerImpl>();
27 +static Microsoft::WRL::ComPtr<WSLCSessionManager> g_sessionManager;
28 +
29 +HRESULT WSLCSessionManagerFactory::CreateInstance(_In_ IUnknown* pUnkOuter, _In_ REFIID riid, _Out_ void** ppCreated)
30 +{
31 + RETURN_HR_IF_NULL(E_POINTER, ppCreated);
32 + *ppCreated = nullptr;
33 +
34 + RETURN_HR_IF(CLASS_E_NOAGGREGATION, pUnkOuter != nullptr);
35 +
36 + WSL_LOG("WSLCSessionManagerFactory", TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
37 +
38 + try
39 + {
40 + std::lock_guard lock{g_mutex};
41 +
42 + THROW_HR_IF(CO_E_SERVER_STOPPING, !g_sessionManagerImpl.has_value());
43 +
44 + if (!g_sessionManager)
45 + {
46 + g_sessionManager = wil::MakeOrThrow<WSLCSessionManager>(&g_sessionManagerImpl.value());
47 + }
48 +
49 + THROW_IF_FAILED(g_sessionManager.CopyTo(riid, ppCreated));
50 + }
51 + catch (...)
52 + {
53 + const auto result = wil::ResultFromCaughtException();
54 +
55 + // Note: S_FALSE will cause COM to retry if the service is stopping.
56 + return result == CO_E_SERVER_STOPPING ? S_FALSE : result;
57 + }
58 +
59 + return S_OK;
60 +}
61 +
62 +void wsl::windows::service::wslc::ClearWslcSessionsAndBlockNewInstances()
63 +{
64 + std::lock_guard lock{g_mutex};
65 +
66 + // Disconnect the COM instance from its implementation.
67 + if (g_sessionManager)
68 + {
69 + g_sessionManager->Disconnect();
70 + g_sessionManager.Reset();
71 +
72 + // N.B. Callers might still have references to the COM instance. If that's the case, calls will all fail with RPC_E_DISCONNECTED.
73 + }
74 +
75 + g_sessionManagerImpl.reset();
76 +}
\ No newline at end of file
src/windows/service/exe/WSLCSessionManagerFactory.h new
+29
@@ -0,0 +1,29 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionManagerFactory.h
8 +
9 +Abstract:
10 +
11 + Contains the definitions for WSLCSessionManagerFactory.
12 +
13 +--*/
14 +
15 +#pragma once
16 +#include <wil/resource.h>
17 +
18 +namespace wsl::windows::service::wslc {
19 +
20 +class WSLCSessionManagerFactory : public Microsoft::WRL::ClassFactory<>
21 +{
22 +public:
23 + WSLCSessionManagerFactory() = default;
24 +
25 + STDMETHODIMP CreateInstance(_In_ IUnknown* pUnkOuter, _In_ REFIID riid, _Out_ void** ppCreated) override;
26 +};
27 +
28 +void ClearWslcSessionsAndBlockNewInstances();
29 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/service/exe/WslCoreVm.cpp
+11 -50
@@ -39,19 +39,11 @@ using namespace std::string_literals;
39 // Start of unaddressable memory if guest only supports the minimum 36-bit addressing.
40 #define MAX_36_BIT_PAGE_IN_MB (0x1000000000 / _1MB)
41
42 -// Version numbers for various functionality that was backported.
43 -#define NICKEL_BUILD_FLOOR 22350
44 -#define VIRTIO_SERIAL_CONSOLE_COBALT_RELEASE_UBR 40
45 -#define VMEMM_SUFFIX_COBALT_REFRESH_BUILD_NUMBER 22138
46 -#define VMMEM_SUFFIX_COBALT_RELEASE_UBR 71
47 -#define VMMEM_SUFFIX_NICKEL_BUILD_NUMBER 22420
48 -
42 #define WSLG_SHARED_MEMORY_SIZE_MB 8192
43 #define PAGE_SIZE 0x1000
44
45 static constexpr size_t c_bootEntropy = 0x1000;
46 static constexpr auto c_localDevicesKey = L"SOFTWARE\\Microsoft\\Terminal Server Client\\LocalDevices";
54 -static constexpr std::pair<uint32_t, uint32_t> c_schemaVersionNickel{2, 7};
47
48 #define LXSS_ENABLE_GUI_APPS() (m_vmConfig.EnableGuiApps && (m_systemDistroDeviceId != ULONG_MAX))
49
@@ -276,14 +268,15 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
268
269 // If the system supports virtio console serial ports, use dmesg capture for telemetry and/or debug output.
270 // Legacy serial is much slower, so this is not enabled without virtio console support.
279 - m_vmConfig.EnableDebugShell &= IsVirtioSerialConsoleSupported();
280 - if (IsVirtioSerialConsoleSupported())
271 + auto enableVirtioSerial = m_vmConfig.EnableVirtio && helpers::IsVirtioSerialConsoleSupported();
272 + m_vmConfig.EnableDebugShell &= enableVirtioSerial;
273 + if (enableVirtioSerial)
274 {
275 try
276 {
277 bool enableTelemetry = TraceLoggingProviderEnabled(g_hTraceLoggingProvider, WINEVENT_LEVEL_INFO, 0);
278 m_dmesgCollector = DmesgCollector::Create(
286 - VmId, m_vmExitEvent, enableTelemetry, m_vmConfig.EnableDebugConsole, m_comPipe0, m_vmConfig.EnableEarlyBootLogging);
279 + VmId, m_vmExitEvent, enableTelemetry, m_vmConfig.EnableDebugConsole, m_comPipe0, m_vmConfig.EnableEarlyBootLogging, {});
280
281 WSL_LOG("DMESG collector created");
282
@@ -365,7 +358,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
358 gpuRequest.RequestType = hcs::ModifyRequestType::Update;
359 gpuRequest.Settings.AssignmentMode = hcs::GpuAssignmentMode::Mirror;
360 gpuRequest.Settings.AllowVendorExtension = true;
368 - if (IsDisableVgpuSettingsSupported())
361 + if (wsl::windows::common::hcs::IsDisableVgpuSettingsSupported())
362 {
363 gpuRequest.Settings.DisableGdiAcceleration = true;
364 gpuRequest.Settings.DisablePresentation = true;
@@ -579,8 +572,9 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
572 {
573 wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::Ipv6;
574 WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::LocalhostRelay, m_vmConfig.EnableLocalhostRelay);
575 + WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::DnsTunnelingSocket, m_vmConfig.EnableDnsTunneling);
576 m_networkingEngine = std::make_unique<wsl::core::VirtioNetworking>(
583 - std::move(gnsChannel), flags, LX_INIT_RESOLVCONF_FULL_HEADER, m_guestDeviceManager, m_userToken);
577 + std::move(gnsChannel), flags, LX_INIT_RESOLVCONF_FULL_HEADER, m_guestDeviceManager, m_userToken, std::move(dnsTunnelingSocket));
578 }
579 else if (m_vmConfig.NetworkingMode == NetworkingMode::Bridged)
580 {
@@ -763,10 +757,7 @@ WslCoreVm::~WslCoreVm() noexcept
757 }
758
759 // Shutdown virtio device hosts.
766 - if (m_guestDeviceManager)
767 - {
768 - m_guestDeviceManager->Shutdown();
769 - }
760 + m_guestDeviceManager.reset();
761
762 // Call RevokeVmAccess on each VHD that was added to the utility VM. This
763 // ensures that the ACL on the VHD does not grow unbounded.
@@ -871,37 +862,6 @@ void WslCoreVm::AddDrvFsShare(_In_ bool Admin, _In_ HANDLE UserToken)
862 }
863 }
864
874 -bool WslCoreVm::IsDisableVgpuSettingsSupported() const
875 -{
876 - // See if the Windows version has the required platform change.
877 - return ((wsl::windows::common::hcs::GetSchemaVersion() >= c_schemaVersionNickel) && (m_windowsVersion.BuildNumber >= 22545));
878 -}
879 -
880 -bool WslCoreVm::IsVirtioSerialConsoleSupported() const
881 -{
882 - if (!m_vmConfig.EnableVirtio)
883 - {
884 - return false;
885 - }
886 -
887 - // See if the Windows version has the required platform change.
888 - //
889 - // N.B. If the package is running on a vibranium or iron build, then it means that lifted
890 - // support is available, so virtio serial is available as well (since it was done in the same LCU).
891 - return m_windowsVersion.BuildNumber != WindowsBuildNumbers::Cobalt ||
892 - m_windowsVersion.UpdateBuildRevision >= VIRTIO_SERIAL_CONSOLE_COBALT_RELEASE_UBR;
893 -}
894 -
895 -bool WslCoreVm::IsVmemmSuffixSupported() const
896 -{
897 - // See if the Windows version has the required platform change.
898 - return (
899 - (m_windowsVersion.BuildNumber >= VMMEM_SUFFIX_NICKEL_BUILD_NUMBER) ||
900 - ((m_windowsVersion.BuildNumber < NICKEL_BUILD_FLOOR) && (m_windowsVersion.BuildNumber >= VMEMM_SUFFIX_COBALT_REFRESH_BUILD_NUMBER)) ||
901 - ((m_windowsVersion.BuildNumber == WindowsBuildNumbers::Cobalt) &&
902 - (m_windowsVersion.UpdateBuildRevision >= VMMEM_SUFFIX_COBALT_RELEASE_UBR)));
903 -}
904 -
865 _Requires_lock_held_(m_guestDeviceLock)
866 void WslCoreVm::AddPlan9Share(
867 _In_ PCWSTR AccessName, _In_ PCWSTR Path, [[maybe_unused]] _In_ UINT32 Port, _In_ hcs::Plan9ShareFlags Flags, _In_ HANDLE UserToken, _In_opt_ PCWSTR VirtIoTag)
@@ -1512,7 +1472,7 @@ std::wstring WslCoreVm::GenerateConfigJson()
1472 vmSettings.ComputeTopology.Processor.Count = m_vmConfig.ProcessorCount;
1473
1474 // Set the vmmem suffix which will change the process name in task manager.
1515 - if (IsVmemmSuffixSupported())
1475 + if (helpers::IsVmemmSuffixSupported())
1476 {
1477 vmSettings.ComputeTopology.Memory.HostingProcessNameSuffix = wsl::windows::common::wslutil::c_vmOwner;
1478 }
@@ -1573,7 +1533,7 @@ std::wstring WslCoreVm::GenerateConfigJson()
1533 kernelCmdLine += L" swiotlb=force";
1534 }
1535
1576 - if (IsVirtioSerialConsoleSupported())
1536 + if (m_vmConfig.EnableVirtio && helpers::IsVirtioSerialConsoleSupported())
1537 {
1538 vmSettings.Devices.VirtioSerial.emplace();
1539 }
@@ -2381,6 +2341,7 @@ void WslCoreVm::RegisterCallbacks(_In_ const std::function<void(ULONG)>& DistroE
2341 const auto* exitMessage = gslhelpers::try_get_struct<LX_MINI_INIT_CHILD_EXIT_MESSAGE>(message);
2342 if (exitMessage)
2343 {
2344 + WSL_LOG("ProcessExited", TraceLoggingValue(exitMessage->ChildPid, "pid"));
2345 exitCallback(exitMessage->ChildPid);
2346 }
2347 }
src/windows/service/exe/WslCoreVm.h
-6
@@ -223,12 +223,6 @@ private:
223
224 bool IsDnsTunnelingSupported() const;
225
226 - bool IsDisableVgpuSettingsSupported() const;
227 -
228 - bool IsVirtioSerialConsoleSupported() const;
229 -
230 - bool IsVmemmSuffixSupported() const;
231 -
226 _Requires_lock_held_(m_lock)
227 DiskMountResult MountDiskLockHeld(
228 _In_ PCWSTR Disk, _In_ DiskType MountDiskType, _In_ ULONG PartitionIndex, _In_opt_ PCWSTR Name, _In_opt_ PCWSTR Type, _In_opt_ PCWSTR Options);
src/windows/service/inc/CMakeLists.txt
+3 -2
@@ -1,2 +1,3 @@
1 -add_idl(wslserviceidl "wslservice.idl" "windowsdefs.idl")
2 -set_target_properties(wslserviceidl PROPERTIES FOLDER windows)
\ No newline at end of file
1 +add_idl(wslserviceidl "wslservice.idl;wslc.idl" "windowsdefs.idl")
2 +
3 +set_target_properties(wslserviceidl PROPERTIES FOLDER windows)
src/windows/service/inc/wslc.idl new
+844
@@ -0,0 +1,844 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft Corporation. All rights reserved.
4 +
5 +Module Name:
6 +
7 + wslc.idl
8 +
9 +Abstract:
10 +
11 + This file contains the WSLC-related COM object definitions.
12 +
13 +--*/
14 +
15 +import "unknwn.idl";
16 +import "wtypes.idl";
17 +
18 +cpp_quote("#ifdef __cplusplus")
19 +cpp_quote("class DECLSPEC_UUID(\"a9b7a1b9-0671-405c-95f1-e0612cb4ce8f\") WSLCSessionManager;")
20 +cpp_quote("class DECLSPEC_UUID(\"9FCD2067-9FC6-4EFA-9EB0-698169EBF7D3\") WSLCSessionFactory;")
21 +cpp_quote("#endif")
22 +
23 +#define WSLC_MAX_CONTAINER_NAME_LENGTH 255
24 +#define WSLC_MAX_IMAGE_NAME_LENGTH 255
25 +#define WSLC_MAX_VOLUME_NAME_LENGTH 255
26 +#define WSLC_MAX_VOLUME_DRIVER_LENGTH 255
27 +#define WSLC_MAX_NETWORK_NAME_LENGTH 255
28 +#define WSLC_CONTAINER_ID_LENGTH 64
29 +#define WSLC_MAX_BINDING_ADDRESS_LENGTH 45
30 +
31 +cpp_quote("#define WSLC_MAX_CONTAINER_NAME_LENGTH 255")
32 +cpp_quote("#define WSLC_MAX_IMAGE_NAME_LENGTH 255")
33 +cpp_quote("#define WSLC_MAX_VOLUME_NAME_LENGTH 255")
34 +cpp_quote("#define WSLC_MAX_VOLUME_DRIVER_LENGTH 255")
35 +cpp_quote("#define WSLC_MAX_NETWORK_NAME_LENGTH 255")
36 +cpp_quote("#define WSLC_CONTAINER_ID_LENGTH 64")
37 +cpp_quote("#define WSLC_MAX_BINDING_ADDRESS_LENGTH 45")
38 +cpp_quote("#define WSLC_EPHEMERAL_PORT 0")
39 +
40 +typedef
41 +struct _WSLCVersion {
42 + ULONG Major;
43 + ULONG Minor;
44 + ULONG Revision;
45 +} WSLCVersion;
46 +
47 +typedef enum _WSLCVirtualMachineTerminationReason
48 +{
49 + WSLCVirtualMachineTerminationReasonUnknown,
50 + WSLCVirtualMachineTerminationReasonShutdown,
51 + WSLCVirtualMachineTerminationReasonCrashed,
52 +} WSLCVirtualMachineTerminationReason;
53 +
54 +typedef enum _WSLCFD
55 +{
56 + WSLCFDStdin = 0,
57 + WSLCFDStdout = 1,
58 + WSLCFDStderr = 2,
59 + WSLCFDTty = 3,
60 +} WSLCFD;
61 +
62 +typedef enum _WSLCSignal
63 +{
64 + WSLCSignalNone = 0,
65 + WSLCSignalSIGHUP = 1,
66 + WSLCSignalSIGINT = 2,
67 + WSLCSignalSIGQUIT = 3,
68 + WSLCSignalSIGILL = 4,
69 + WSLCSignalSIGTRAP = 5,
70 + WSLCSignalSIGABRT = 6,
71 + WSLCSignalSIGIOT = 6, // SIGABRT and SIGIOT are equivalent.
72 + WSLCSignalSIGBUS = 7,
73 + WSLCSignalSIGFPE = 8,
74 + WSLCSignalSIGKILL = 9,
75 + WSLCSignalSIGUSR1 = 10,
76 + WSLCSignalSIGSEGV = 11,
77 + WSLCSignalSIGUSR2 = 12,
78 + WSLCSignalSIGPIPE = 13,
79 + WSLCSignalSIGALRM = 14,
80 + WSLCSignalSIGTERM = 15,
81 + WSLCSignalSIGTKFLT = 16,
82 + WSLCSignalSIGCHLD = 17,
83 + WSLCSignalSIGCONT = 18,
84 + WSLCSignalSIGSTOP = 19,
85 + WSLCSignalSIGTSTP = 20,
86 + WSLCSignalSIGTTIN = 21,
87 + WSLCSignalSIGTTOU = 22,
88 + WSLCSignalSIGURG = 23,
89 + WSLCSignalSIGXCPU = 24,
90 + WSLCSignalSIGXFSZ = 25,
91 + WSLCSignalSIGVTALRM = 26,
92 + WSLCSignalSIGPROF = 27,
93 + WSLCSignalSIGWINCH = 28,
94 + WSLCSignalSIGIO = 29,
95 + WSLCSignalSIGPOLL = 29, // SIGIO and SIGPOLL are equivalent.
96 + WSLCSignalSIGPWR = 30,
97 + WSLCSignalSIGSYS = 31
98 +} WSLCSignal;
99 +
100 +[
101 + uuid(7BC4E198-6531-4FA6-ADE2-5EF3D2A04DFE),
102 + pointer_default(unique),
103 + object
104 +]
105 +interface ITerminationCallback : IUnknown
106 +{
107 + HRESULT OnTermination(WSLCVirtualMachineTerminationReason Reason, LPCWSTR Details);
108 +};
109 +
110 +[
111 + uuid(5038842F-53DB-4F30-A6D0-A41B02C94AC1),
112 + pointer_default(unique),
113 + object
114 +]
115 +interface IProgressCallback : IUnknown
116 +{
117 + HRESULT OnProgress(LPCSTR Status, LPCSTR Id, ULONGLONG Current, ULONGLONG Total);
118 +};
119 +
120 +typedef struct _WSLCImageInformation
121 +{
122 + char Image[WSLC_MAX_IMAGE_NAME_LENGTH + 1];
123 + char Hash[256];
124 + char Digest[256];
125 + ULONGLONG Size;
126 + LONGLONG Created; // Unix timestamp
127 + char ParentId[256];
128 +} WSLCImageInformation;
129 +
130 +typedef enum _WSLCListImagesFlags
131 +{
132 + WSLCListImagesFlagsNone = 0,
133 + WSLCListImagesFlagsAll = 1, // Show all images (default hides intermediate images)
134 + WSLCListImagesFlagsDigests = 2, // Include digest information
135 + WSLCListImagesFlagsDanglingTrue = 4, // Show only dangling images (untagged)
136 + WSLCListImagesFlagsDanglingFalse = 8, // Show only non-dangling images (tagged)
137 + // Note: If neither dangling flag is set, no dangling filter is passed (default: both).
138 +} WSLCListImagesFlags;
139 +
140 +cpp_quote("#define WSLCListImagesFlagsValid (WSLCListImagesFlagsAll | WSLCListImagesFlagsDigests | WSLCListImagesFlagsDanglingTrue | WSLCListImagesFlagsDanglingFalse)")
141 +
142 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCListImagesFlags);")
143 +
144 +typedef struct _KeyValuePairInformation
145 +{
146 + [string] LPSTR Key;
147 + [string] LPSTR Value;
148 +} KeyValuePairInformation;
149 +
150 +typedef struct _KeyValuePair
151 +{
152 + [string] LPCSTR Key;
153 + [string] LPCSTR Value;
154 +} KeyValuePair;
155 +
156 +typedef KeyValuePair WSLCLabel;
157 +typedef KeyValuePair WSLCDriverOption;
158 +
159 +typedef KeyValuePairInformation WSLCLabelInformation;
160 +typedef KeyValuePairInformation WSLCDriverOptionInformation;
161 +
162 +typedef struct _WSLCListImageOptions
163 +{
164 + DWORD Flags; // WSLCListImagesFlags (can combine with bitwise OR)
165 + [unique] LPCSTR Reference; // Filter by reference (name[:tag])
166 + [unique] LPCSTR Before; // Filter: show images created before this image
167 + [unique] LPCSTR Since; // Filter: show images created since this image
168 + [unique, size_is(LabelsCount)] const WSLCLabel* Labels;
169 + ULONG LabelsCount;
170 +} WSLCListImageOptions;
171 +
172 +typedef enum _WSLCProcessFlags
173 +{
174 + WSLCProcessFlagsNone = 0,
175 + WSLCProcessFlagsStdin = 1,
176 + WSLCProcessFlagsTty = 2
177 +} WSLCProcessFlags;
178 +
179 +cpp_quote("#define WSLCProcessFlagsValid (WSLCProcessFlagsStdin | WSLCProcessFlagsTty)")
180 +
181 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCProcessFlags);")
182 +
183 +typedef struct _WSLCStringArray
184 +{
185 + [unique, size_is(Count)] LPCSTR const* Values;
186 + ULONG Count;
187 +} WSLCStringArray;
188 +
189 +typedef struct _WSLCProcessOptions
190 +{
191 + [unique] LPCSTR CurrentDirectory;
192 + [unique] LPCSTR User;
193 + WSLCStringArray CommandLine;
194 + WSLCStringArray Environment;
195 + WSLCProcessFlags Flags;
196 + ULONG TtyRows; // Only needed when tty fd's are passed.
197 + ULONG TtyColumns;
198 +} WSLCProcessOptions;
199 +
200 +typedef struct _WSLCNamedVolume
201 +{
202 + LPCSTR Name;
203 + LPCSTR ContainerPath;
204 + BOOL ReadOnly;
205 +} WSLCNamedVolume;
206 +
207 +typedef struct _WSLCVolume
208 +{
209 + LPCWSTR HostPath;
210 + LPCSTR ContainerPath;
211 + BOOL ReadOnly;
212 +} WSLCVolume;
213 +
214 +typedef struct _WSLCPortMapping
215 +{
216 + USHORT HostPort;
217 + USHORT ContainerPort;
218 + int Family;
219 + int Protocol;
220 + char BindingAddress[WSLC_MAX_BINDING_ADDRESS_LENGTH + 1];
221 +} WSLCPortMapping;
222 +
223 +typedef struct _WSLCTmpfsMount
224 +{
225 + LPCSTR Destination;
226 + [unique] LPCSTR Options;
227 +} WSLCTmpfsMount;
228 +
229 +typedef enum _WSLCContainerNetworkType
230 +{
231 + WSLCContainerNetworkTypeNone = 0,
232 + WSLCContainerNetworkTypeHost = 1,
233 + WSLCContainerNetworkTypeBridged = 2,
234 + // WSLCContainerNetworkTypeCustom = 3 // TODO: Implement when implementing custom networks
235 +} WSLCContainerNetworkType;
236 +
237 +typedef struct _WSLCContainerNetwork
238 +{
239 + WSLCContainerNetworkType ContainerNetworkType;
240 + LPCSTR ContainerNetworkName;
241 +} WSLCContainerNetwork;
242 +
243 +typedef enum _WSLCContainerFlags
244 +{
245 + WSLCContainerFlagsNone = 0,
246 + WSLCContainerFlagsRm = 1, // Delete the container when it exits. TODO: Implement.
247 + WSLCContainerFlagsGpu = 2, // Enable GPU access. TODO: implement.
248 + WSLCContainerFlagsInit = 4, // Run the container under an init process.
249 + WSLCContainerFlagsPublishAll = 8, // Publish all exposed ports.
250 +} WSLCContainerFlags;
251 +
252 +cpp_quote("#define WSLCContainerFlagsValid (WSLCContainerFlagsRm | WSLCContainerFlagsGpu | WSLCContainerFlagsInit | WSLCContainerFlagsPublishAll)")
253 +
254 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCContainerFlags);")
255 +
256 +
257 +typedef enum _WSLCContainerStartFlags
258 +{
259 + WSLCContainerStartFlagsNone = 0,
260 + WSLCContainerStartFlagsAttach = 1, // Attach stdio handles on start.
261 +} WSLCContainerStartFlags;
262 +
263 +cpp_quote("#define WSLCContainerStartFlagsValid (WSLCContainerStartFlagsAttach)")
264 +
265 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCContainerStartFlags);")
266 +
267 +typedef struct _WSLCContainerOptions
268 +{
269 + LPCSTR Image;
270 + [unique] LPCSTR Name;
271 + WSLCStringArray Entrypoint;
272 + WSLCProcessOptions InitProcessOptions;
273 + [unique, size_is(VolumesCount)] WSLCVolume* Volumes;
274 + ULONG VolumesCount;
275 + [unique, size_is(PortsCount)] WSLCPortMapping* Ports;
276 + ULONG PortsCount;
277 + [unique, size_is(LabelsCount)] const WSLCLabel* Labels;
278 + ULONG LabelsCount;
279 + WSLCContainerFlags Flags;
280 + WSLCSignal StopSignal;
281 + // TODO: List specific GPU devices.
282 + [unique] LPCSTR HostName;
283 + [unique] LPCSTR DomainName;
284 +
285 + WSLCStringArray DnsServers;
286 + WSLCStringArray DnsSearchDomains;
287 + WSLCStringArray DnsOptions;
288 +
289 + ULONGLONG ShmSize;
290 + WSLCContainerNetwork ContainerNetwork;
291 + [unique, size_is(TmpfsCount)] const WSLCTmpfsMount* Tmpfs;
292 + ULONG TmpfsCount;
293 +
294 + [unique, size_is(NamedVolumesCount)] WSLCNamedVolume* NamedVolumes;
295 + ULONG NamedVolumesCount;
296 +} WSLCContainerOptions;
297 +
298 +typedef enum _WSLCContainerState
299 +{
300 + WslcContainerStateInvalid = 0,
301 + WslcContainerStateCreated = 1,
302 + WslcContainerStateRunning = 2,
303 + WslcContainerStateExited = 3,
304 + WslcContainerStateDeleted = 4,
305 +} WSLCContainerState;
306 +
307 +typedef char WSLCContainerId[WSLC_CONTAINER_ID_LENGTH + 1] ;
308 +
309 +typedef struct _WSLCContainerEntry
310 +{
311 + char Name[WSLC_MAX_CONTAINER_NAME_LENGTH + 1];
312 + char Image[WSLC_MAX_IMAGE_NAME_LENGTH + 1];
313 + WSLCContainerId Id;
314 + ULONGLONG StateChangedAt;
315 + ULONGLONG CreatedAt;
316 + WSLCContainerState State;
317 +} WSLCContainerEntry;
318 +
319 +typedef struct _WSLCContainerPortMapping
320 +{
321 + WSLCContainerId Id;
322 + WSLCPortMapping PortMapping;
323 +} WSLCContainerPortMapping;
324 +
325 +typedef [system_handle(sh_file)] HANDLE FILE_HANDLE;
326 +typedef [system_handle(sh_pipe)] HANDLE PIPE_HANDLE;
327 +typedef [system_handle(sh_socket)] HANDLE SOCKET_HANDLE;
328 +
329 +typedef enum _WSLCHandleType
330 +{
331 + WSLCHandleTypeUnknown = 0,
332 + WSLCHandleTypeFile = 1,
333 + WSLCHandleTypePipe = 2,
334 + WSLCHandleTypeSocket = 3
335 +} WSLCHandleType;
336 +
337 +typedef struct _WSLCHandle
338 +{
339 + WSLCHandleType Type;
340 +
341 + [switch_type(WSLCHandleType), switch_is(Type)]
342 + union
343 + {
344 + [case(WSLCHandleTypeFile)]
345 + FILE_HANDLE File;
346 + [case(WSLCHandleTypePipe)]
347 + PIPE_HANDLE Pipe;
348 + [case(WSLCHandleTypeSocket)]
349 + SOCKET_HANDLE Socket;
350 + [default];
351 + } Handle;
352 +} WSLCHandle;
353 +
354 +typedef enum _WSLCProcessState
355 +{
356 + WslcProcessStateUnknown = 0,
357 + WslcProcessStateRunning = 1,
358 + WslcProcessStateExited = 2,
359 + WslcProcessStateSignalled = 3
360 +} WSLCProcessState;
361 +
362 +[
363 + uuid(1AD163CD-393D-4B33-83A2-8A3F3F23E608),
364 + pointer_default(unique),
365 + object
366 +]
367 +interface IWSLCProcess : IUnknown
368 +{
369 + HRESULT Signal([in] int Signal);
370 + HRESULT GetExitEvent([out, system_handle(sh_event)] HANDLE* EventHandle);
371 + HRESULT GetStdHandle([in] WSLCFD Fd, [out] WSLCHandle* Handle);
372 + HRESULT GetFlags([out] WSLCProcessFlags* Flags);
373 + HRESULT GetPid([out] int* Pid);
374 + HRESULT GetState([out] WSLCProcessState* State, [out] int* Code);
375 + HRESULT ResizeTty([in] ULONG Rows, [in] ULONG Columns);
376 +
377 + // Note: the SDK can offer a convenience Wait() method, but that doesn't need to be part of the service API.
378 +}
379 +
380 +typedef enum _WSLCNetworkingMode
381 +{
382 + WSLCNetworkingModeNone,
383 + WSLCNetworkingModeNAT,
384 + WSLCNetworkingModeVirtioProxy
385 +} WSLCNetworkingMode;
386 +
387 +typedef enum _WSLCFeatureFlags
388 +{
389 + WslcFeatureFlagsNone = 0,
390 + WslcFeatureFlagsDnsTunneling = 1,
391 + WslcFeatureFlagsEarlyBootDmesg = 2,
392 + WslcFeatureFlagsGPU = 4,
393 + WslcFeatureFlagsVirtioFs = 8,
394 + WslcFeatureFlagsDebug = 16,
395 +} WSLCFeatureFlags;
396 +
397 +cpp_quote("#define WSLCFeatureFlagsValid (WslcFeatureFlagsDnsTunneling | WslcFeatureFlagsEarlyBootDmesg | WslcFeatureFlagsGPU | WslcFeatureFlagsVirtioFs | WslcFeatureFlagsDebug)")
398 +
399 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCFeatureFlags);")
400 +
401 +//
402 +// IWSLCVirtualMachine - Interface representing a single VM instance.
403 +// Operations are scoped to this VM. The VM ID is stored internally,
404 +// so only the holder of this interface can operate on the VM.
405 +//
406 +[
407 + uuid(B5E2D8F1-9A3C-4E6B-8D1F-7C4A2E9B6D3A),
408 + pointer_default(unique),
409 + object
410 +]
411 +interface IWSLCVirtualMachine : IUnknown
412 +{
413 + // Gets the VM ID.
414 + HRESULT GetId([out, retval] GUID* VmId);
415 +
416 + // Accepts a connect from mini_init in the VM.
417 + HRESULT AcceptConnection([out, system_handle(sh_socket)] HANDLE* Socket);
418 +
419 + // Configures networking engine with sockets from the user process.
420 + // GnsSocket is required; DnsSocket is optional (NULL if DNS tunneling is disabled).
421 + // The service duplicates the socket handles.
422 + HRESULT ConfigureNetworking(
423 + [in, system_handle(sh_socket)] HANDLE GnsSocket,
424 + [in, system_handle(sh_socket), unique] HANDLE* DnsSocket);
425 +
426 + // Attaches a VHD or VHDX disk to the VM.
427 + // GrantVmAccess is called by the service before attaching.
428 + // Returns the SCSI LUN assigned to the disk.
429 + HRESULT AttachDisk([in] LPCWSTR Path, [in] BOOL ReadOnly, [out, retval] ULONG* Lun);
430 +
431 + // Detaches a previously attached disk from the VM.
432 + HRESULT DetachDisk([in] ULONG Lun);
433 +
434 + // Adds a filesystem share (Plan9 or VirtioFS) accessible to the VM.
435 + // Returns an instance GUID that can be used to remove the share.
436 + HRESULT AddShare([in] LPCWSTR WindowsPath, [in] BOOL ReadOnly, [out, retval] GUID* ShareId);
437 +
438 + // Removes a previously added filesystem share.
439 + HRESULT RemoveShare([in] REFGUID ShareId);
440 +
441 + // Returns an event that is signaled when the VM exits (graceful or forced).
442 + HRESULT GetTerminationEvent([out, system_handle(sh_event)] HANDLE* Event);
443 +}
444 +
445 +typedef enum _WSLCSessionStorageFlags
446 +{
447 + WSLCSessionStorageFlagsNone = 0,
448 + WSLCSessionStorageFlagsNoCreate = 1, // Open an existing storage path, but don't create a new one.
449 + WSLCSessionStorageFlagsValid = WSLCSessionStorageFlagsNoCreate
450 +} WSLCSessionStorageFlags;
451 +
452 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCSessionStorageFlags);")
453 +
454 +// Settings for IWSLCSessionManager::CreateSession - full session configuration
455 +typedef struct _WSLCSessionSettings {
456 + LPCWSTR DisplayName;
457 + LPCWSTR StoragePath;
458 + ULONGLONG MaximumStorageSizeMb;
459 + ULONG CpuCount;
460 + ULONG MemoryMb;
461 + ULONG BootTimeoutMs;
462 + WSLCNetworkingMode NetworkingMode;
463 + [unique] ITerminationCallback* TerminationCallback;
464 + WSLCFeatureFlags FeatureFlags;
465 + WSLCHandle DmesgOutput;
466 + WSLCSessionStorageFlags StorageFlags;
467 +
468 + // Below options are used for debugging purposes only.
469 + [unique] LPCWSTR RootVhdOverride;
470 + [unique] LPCSTR RootVhdTypeOverride;
471 +} WSLCSessionSettings;
472 +
473 +typedef enum _WSLCLogsFlags
474 +{
475 + WSLCLogsFlagsNone = 0,
476 + WSLCLogsFlagsFollow = 1,
477 + WSLCLogsFlagsTimestamps = 2,
478 +} WSLCLogsFlags;
479 +
480 +cpp_quote("#define WSLCLogsFlagsValid (WSLCLogsFlagsFollow | WSLCLogsFlagsTimestamps)")
481 +
482 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCLogsFlags);")
483 +
484 +typedef enum _WSLCDeleteFlags
485 +{
486 + WSLCDeleteFlagsNone = 0,
487 + WSLCDeleteFlagsForce = 1,
488 + WSLCDeleteFlagsDeleteVolumes = 2,
489 + // TODO: Flags to remove bridge links, etc.
490 +} WSLCDeleteFlags;
491 +
492 +cpp_quote("#define WSLCDeleteFlagsValid (WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes)")
493 +
494 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCDeleteFlags);")
495 +
496 +
497 +[
498 + uuid(7577FE8D-DE85-471E-B870-11669986F332),
499 + pointer_default(unique),
500 + object
501 +]
502 +interface IWSLCContainer : IUnknown
503 +{
504 + HRESULT Attach([in, unique] LPCSTR DetachKeys, [out] WSLCHandle* StdIn, [out] WSLCHandle* StdOut, [out] WSLCHandle* StdErr);
505 + HRESULT Stop([in] WSLCSignal Signal, [in] LONG TimeoutSeconds);
506 + HRESULT Start([in] WSLCContainerStartFlags Flags, [in, unique] LPCSTR DetachKeys);
507 + HRESULT Delete([in] WSLCDeleteFlags Flags);
508 + HRESULT Export([in] WSLCHandle TarHandle);
509 + HRESULT GetState([out] WSLCContainerState* State);
510 + HRESULT GetInitProcess([out] IWSLCProcess** Process);
511 + HRESULT Exec([in, ref] const WSLCProcessOptions* Options, [in, unique] LPCSTR DetachKeys, [out] IWSLCProcess** Process);
512 + HRESULT Inspect([out] LPSTR* Output);
513 + HRESULT Logs([in] WSLCLogsFlags Flags, [out] WSLCHandle* Stdout, [out] WSLCHandle* Stderr, [in] ULONGLONG Since, [in] ULONGLONG Until, [in] ULONGLONG Tail);
514 + HRESULT GetId([out, string] WSLCContainerId Id);
515 + HRESULT GetName([out, string] LPSTR* Name);
516 + HRESULT GetLabels([out, size_is(, *Count)] WSLCLabelInformation** Labels, [out] ULONG* Count);
517 + HRESULT Kill([in] WSLCSignal Signal);
518 +}
519 +
520 +typedef enum _WSLCDeletedImageType
521 +{
522 + WSLCDeletedImageTypeDeleted = 0,
523 + WSLCDeletedImageTypeUntagged = 1
524 +} WSLCDeletedImageType;
525 +
526 +typedef struct _WSLCDeletedImageInformation
527 +{
528 + char Image[WSLC_MAX_IMAGE_NAME_LENGTH + 1];
529 + WSLCDeletedImageType Type;
530 +} WSLCDeletedImageInformation;
531 +
532 +typedef enum _WSLCDeleteImageFlags
533 +{
534 + WSLCDeleteImageFlagsNone = 0,
535 + WSLCDeleteImageFlagsForce = 1,
536 + WSLCDeleteImageFlagsNoPrune = 2,
537 +} WSLCDeleteImageFlags;
538 +
539 +cpp_quote("#define WSLCDeleteImageFlagsValid (WSLCDeleteImageFlagsForce | WSLCDeleteImageFlagsNoPrune)")
540 +
541 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCDeleteImageFlags);")
542 +
543 +typedef struct _WSLCDeleteImageOptions
544 +{
545 + LPCSTR Image; // Image can be ID or Repo:Tag.
546 + DWORD Flags; // WSLCDeleteImageFlags
547 + // TODO: Platforms: a json array of OCI platform strings.
548 +} WSLCDeleteImageOptions;
549 +
550 +typedef enum _WSLCBuildImageFlags
551 +{
552 + WSLCBuildImageFlagsNone = 0,
553 + WSLCBuildImageFlagsVerbose = 1, // Show all build progress including internal steps.
554 + WSLCBuildImageFlagsNoCache = 2, // Do not use cache when building the image.
555 + WSLCBuildImageFlagsPull = 4, // Always attempt to pull a newer version of the image.
556 +} WSLCBuildImageFlags;
557 +
558 +cpp_quote("#define WSLCBuildImageFlagsValid (WSLCBuildImageFlagsVerbose | WSLCBuildImageFlagsNoCache | WSLCBuildImageFlagsPull)")
559 +
560 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCBuildImageFlags);")
561 +
562 +typedef struct _WSLCBuildImageOptions
563 +{
564 + LPCWSTR ContextPath;
565 + WSLCHandle DockerfileHandle;
566 + WSLCStringArray Tags;
567 + WSLCStringArray BuildArgs; // KEY=VALUE pairs passed as --build-arg to docker.
568 + LPCSTR Target; // Target build stage name passed as --target to docker.
569 + WSLCBuildImageFlags Flags; // WSLCBuildImageFlags
570 +} WSLCBuildImageOptions;
571 +
572 +typedef struct _WSLCTagImageOptions
573 +{
574 + LPCSTR Image; // Source image name or ID.
575 + LPCSTR Repo; // Target repository name.
576 + LPCSTR Tag; // Target tag name.
577 +} WSLCTagImageOptions;
578 +
579 +typedef struct _WSLCVolumeOptions
580 +{
581 + [unique] LPCSTR Name;
582 + [unique] LPCSTR Driver;
583 + [unique, size_is(DriverOptsCount)] const WSLCDriverOption* DriverOpts;
584 + ULONG DriverOptsCount;
585 + [unique, size_is(LabelsCount)] const WSLCLabel* Labels;
586 + ULONG LabelsCount;
587 +} WSLCVolumeOptions;
588 +
589 +typedef char WSLCVolumeName[WSLC_MAX_VOLUME_NAME_LENGTH + 1];
590 +
591 +typedef struct _WSLCVolumeInformation
592 +{
593 + WSLCVolumeName Name;
594 + char Driver[WSLC_MAX_VOLUME_DRIVER_LENGTH + 1];
595 +} WSLCVolumeInformation;
596 +
597 +typedef struct _WSLCPruneVolumesResults
598 +{
599 + [unique, size_is(VolumesCount)] WSLCVolumeName* Volumes;
600 + ULONG VolumesCount;
601 + ULONGLONG SpaceReclaimed;
602 +} WSLCPruneVolumesResults;
603 +
604 +typedef struct _WSLCNetworkOptions
605 +{
606 + LPCSTR Name;
607 + LPCSTR Driver;
608 + [unique, size_is(DriverOptsCount)] const WSLCDriverOption* DriverOpts;
609 + ULONG DriverOptsCount;
610 + [unique, size_is(LabelsCount)] const WSLCLabel* Labels;
611 + ULONG LabelsCount;
612 +} WSLCNetworkOptions;
613 +
614 +typedef struct _WSLCNetworkInformation
615 +{
616 + char Name[WSLC_MAX_NETWORK_NAME_LENGTH + 1];
617 + char Id[WSLC_CONTAINER_ID_LENGTH + 1];
618 + char Driver[64];
619 +} WSLCNetworkInformation;
620 +
621 +typedef struct _WSLCPruneLabelFilter
622 +{
623 + LPCSTR Key;
624 + [unique] LPCSTR Value;
625 + BOOL Present;
626 +} WSLCPruneLabelFilter;
627 +
628 +typedef struct _WSLCPruneVolumesOptions
629 +{
630 + BOOL All; // If TRUE, prune all unused volumes. If FALSE, only anonymous volumes.
631 + [unique, size_is(LabelsCount)] const WSLCPruneLabelFilter* Labels;
632 + ULONG LabelsCount;
633 +} WSLCPruneVolumesOptions;
634 +
635 +typedef struct _WSLCPruneContainersResults
636 +{
637 + [unique, size_is(ContainersCount)] WSLCContainerId* Containers;
638 + ULONG ContainersCount;
639 + ULONGLONG SpaceReclaimed;
640 +} WSLCPruneContainersResults;
641 +
642 +typedef enum _WSLCPruneImagesFlags
643 +{
644 + WSLCPruneImagesFlagsNone = 0,
645 + WSLCPruneImagesFlagsDanglingTrue = 1, // Only prune dangling (untagged) images.
646 + WSLCPruneImagesFlagsDanglingFalse = 2, // Prune all images not used by any container.
647 + // Note: If neither dangling flag is set, no dangling filter is passed (Docker defaults to dangling-only).
648 +} WSLCPruneImagesFlags;
649 +
650 +cpp_quote("#define WSLCPruneImagesFlagsValid (WSLCPruneImagesFlagsDanglingTrue | WSLCPruneImagesFlagsDanglingFalse)")
651 +
652 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCPruneImagesFlags);")
653 +
654 +typedef struct _WSLCPruneImagesOptions
655 +{
656 + DWORD Flags; // WSLCPruneImagesFlags
657 + ULONGLONG Until;
658 + [unique, size_is(LabelsCount)] const WSLCPruneLabelFilter* Labels;
659 + ULONG LabelsCount;
660 +} WSLCPruneImagesOptions;
661 +
662 +typedef enum _WSLCSessionState
663 +{
664 + WSLCSessionStateRunning = 0,
665 + WSLCSessionStateTerminated = 1
666 +} WSLCSessionState;
667 +
668 +// Settings for IWSLCSession::Initialize - passed from service to per-user process
669 +typedef struct _WSLCSessionInitSettings
670 +{
671 + ULONG SessionId;
672 + ULONG CreatorPid;
673 + LPCWSTR DisplayName;
674 + LPCWSTR StoragePath;
675 + WSLCSessionStorageFlags StorageFlags;
676 + ULONGLONG MaximumStorageSizeMb;
677 + ULONG BootTimeoutMs;
678 + WSLCNetworkingMode NetworkingMode;
679 + WSLCFeatureFlags FeatureFlags;
680 + [unique] LPCSTR RootVhdTypeOverride;
681 +} WSLCSessionInitSettings;
682 +
683 +[
684 + uuid(EF0661E4-6364-40EA-B433-E2FDF11F3519),
685 + pointer_default(unique),
686 + object
687 +]
688 +interface IWSLCSession : IUnknown
689 +{
690 + HRESULT GetId([out] ULONG* Id);
691 + HRESULT GetState([out] WSLCSessionState* State);
692 +
693 + // Image management.
694 + HRESULT PullImage([in] LPCSTR Image, [in, unique] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback);
695 + HRESULT BuildImage([in] const WSLCBuildImageOptions* Options, [in, unique] IProgressCallback* ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
696 + HRESULT LoadImage([in] WSLCHandle ImageHandle, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength);
697 + HRESULT ImportImage([in] WSLCHandle ImageHandle, [in] LPCSTR ImageName, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength);
698 + HRESULT SaveImage([in] WSLCHandle OutputHandle, [in] LPCSTR ImageNameOrID, [in, unique] IProgressCallback * ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
699 + HRESULT ListImages([in, unique] const WSLCListImageOptions* Options, [out, size_is(, *Count)] WSLCImageInformation** Images, [out] ULONG* Count);
700 + HRESULT DeleteImage([in] const WSLCDeleteImageOptions* Options, [out, size_is(, *Count)] WSLCDeletedImageInformation** DeletedImages, [out] ULONG* Count);
701 + HRESULT TagImage([in] const WSLCTagImageOptions* Options);
702 + HRESULT InspectImage([in] LPCSTR ImageNameOrId, [out] LPSTR* Output);
703 + HRESULT PruneImages([in, unique] const WSLCPruneImagesOptions* Options, [out, size_is(, *DeletedImagesCount)] WSLCDeletedImageInformation** DeletedImages, [out] ULONG* DeletedImagesCount, [out] ULONGLONG* SpaceReclaimed);
704 +
705 + // Container management.
706 + HRESULT CreateContainer([in] const WSLCContainerOptions* Options, [out] IWSLCContainer** Container);
707 + HRESULT OpenContainer([in, ref] LPCSTR Id, [out] IWSLCContainer** Container);
708 + HRESULT ListContainers([out, size_is(, *Count)] WSLCContainerEntry** Containers,
709 + [out] ULONG* Count,
710 + [out, size_is(, *PortsCount)] WSLCContainerPortMapping** Ports,
711 + [out] ULONG* PortsCount);
712 + HRESULT PruneContainers([in, unique, size_is(FiltersCount)] WSLCPruneLabelFilter* Filters, [in] DWORD FiltersCount, [in] ULONGLONG Until, [out] WSLCPruneContainersResults* Result);
713 +
714 + // Create a process at the VM level. This is meant for debugging.
715 + HRESULT CreateRootNamespaceProcess([in, ref] LPCSTR Executable, [in, ref] const WSLCProcessOptions* Options, [out] IWSLCProcess** Process, [out] int* Errno);
716 +
717 + // TODO: an OpenProcess() method can be added later if needed.
718 +
719 + // Disk management.
720 + HRESULT FormatVirtualDisk([in, ref] LPCWSTR Path);
721 +
722 + // Terminate the VM and containers.
723 + HRESULT Terminate();
724 +
725 + // Used only for testing. TODO: Think about moving them to a dedicated testing-only interface.
726 + HRESULT MountWindowsFolder([in, ref] LPCWSTR WindowsPath, [in, ref] LPCSTR LinuxPath, [in] BOOL ReadOnly);
727 + HRESULT UnmountWindowsFolder([in, ref] LPCSTR LinuxPath);
728 + HRESULT MapVmPort([in] int Family, [in] unsigned short WindowsPort, [in] unsigned short LinuxPort);
729 + HRESULT UnmapVmPort([in] int Family, [in] unsigned short WindowsPort, [in] unsigned short LinuxPort);
730 +
731 + // Session initialization - called by SYSTEM service after launching per-user process.
732 + // Returns a handle to this COM server process (used to add to job object).
733 + HRESULT GetProcessHandle([out, system_handle(sh_process)] HANDLE* ProcessHandle);
734 +
735 + // Initializes the session with a pre-created VM.
736 + HRESULT Initialize(
737 + [in] const WSLCSessionInitSettings* Settings,
738 + [in] IWSLCVirtualMachine* Vm);
739 +
740 + // Volume management.
741 + HRESULT CreateVolume([in] const WSLCVolumeOptions* Options, [out] WSLCVolumeInformation* VolumeInfo);
742 + HRESULT DeleteVolume([in] LPCSTR Name);
743 + HRESULT ListVolumes([out, size_is(, *Count)] WSLCVolumeInformation** Volumes, [out] ULONG* Count);
744 + HRESULT InspectVolume([in] LPCSTR Name, [out] LPSTR* Output);
745 +
746 + HRESULT Authenticate([in] LPCSTR ServerAddress, [in] LPCSTR Username, [in] LPCSTR Password, [out] LPSTR* IdentityToken);
747 + HRESULT PushImage([in] LPCSTR Image, [in] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback);
748 + HRESULT PruneVolumes([in, unique] const WSLCPruneVolumesOptions* Options, [out] WSLCPruneVolumesResults* Results);
749 +
750 + // Network management.
751 + HRESULT CreateNetwork([in] const WSLCNetworkOptions* Options);
752 + HRESULT DeleteNetwork([in] LPCSTR Name);
753 + HRESULT ListNetworks([out, size_is(, *Count)] WSLCNetworkInformation** Networks, [out] ULONG* Count);
754 + HRESULT InspectNetwork([in] LPCSTR Name, [out] LPSTR* Output);
755 +}
756 +
757 +//
758 +// IWSLCSessionReference - Weak reference to a session held by the SYSTEM service.
759 +// Stored in per-user process, allows service to check liveness and terminate sessions.
760 +// Session metadata (ID, name, etc.) is stored service-side in SessionEntry.
761 +//
762 +[
763 + uuid(B3A72F48-9D15-4E8A-A621-7C3E84F09B52),
764 + pointer_default(unique),
765 + object
766 +]
767 +interface IWSLCSessionReference : IUnknown
768 +{
769 + // Try to open the session. Fails if session was released or terminated.
770 + // Returns S_OK and a valid session if still alive.
771 + HRESULT OpenSession([out] IWSLCSession** Session);
772 +
773 + // Terminate the session if still alive.
774 + HRESULT Terminate();
775 +}
776 +
777 +//
778 +// IWSLCSessionFactory - Creates sessions in the per-user COM server process.
779 +// Called by the SYSTEM service via CoCreateInstanceAsUser.
780 +//
781 +[
782 + uuid(C4E8F291-3B5D-4A7C-9E12-8F6A4D2B7C91),
783 + pointer_default(unique),
784 + object
785 +]
786 +interface IWSLCSessionFactory : IUnknown
787 +{
788 + // Creates a new session and returns both the session interface and a service reference.
789 + HRESULT CreateSession(
790 + [in] const WSLCSessionInitSettings* Settings,
791 + [in] IWSLCVirtualMachine* Vm,
792 + [out] IWSLCSession** Session,
793 + [out] IWSLCSessionReference** ServiceRef);
794 +
795 + // Gets the process handle for adding to job object.
796 + HRESULT GetProcessHandle([out, system_handle(sh_process)] HANDLE* ProcessHandle);
797 +}
798 +
799 +typedef struct _WSLCSessionInformation
800 +{
801 + ULONG SessionId;
802 + DWORD CreatorPid;
803 + wchar_t DisplayName[256];
804 + wchar_t Sid[256 + 1]; // MAX_SID_SIZE = 256
805 +} WSLCSessionInformation;
806 +
807 +typedef enum _WSLCSessionFlags
808 +{
809 + WSLCSessionFlagsNone = 0,
810 + WSLCSessionFlagsPersistent = 1, // Session remains active after its COM reference is released.
811 + WSLCSessionFlagsOpenExisting = 2, // Open an existing session if the name is in use.
812 +} WSLCSessionFlags;
813 +
814 +cpp_quote("#define WSLCSessionFlagsValid (WSLCSessionFlagsPersistent | WSLCSessionFlagsOpenExisting)")
815 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCSessionFlags);")
816 +
817 +[
818 + uuid(82A7ABC8-6B50-43FC-AB96-15FBBE7E8760),
819 + pointer_default(unique),
820 + object
821 +]
822 +interface IWSLCSessionManager : IUnknown
823 +{
824 + HRESULT GetVersion([out] WSLCVersion* Version);
825 +
826 + // Session management.
827 + HRESULT CreateSession([in, unique] const WSLCSessionSettings* Settings, WSLCSessionFlags Flags, [out] IWSLCSession** Session);
828 + HRESULT EnterSession([in, ref] LPCWSTR DisplayName, [in, ref] LPCWSTR StoragePath, [out] IWSLCSession** Session);
829 + HRESULT ListSessions([out, size_is(, *SessionsCount)] WSLCSessionInformation** Sessions, [out] ULONG* SessionsCount);
830 + HRESULT OpenSession([in] ULONG Id, [out] IWSLCSession** Session);
831 + HRESULT OpenSessionByName([in, unique] LPCWSTR DisplayName, [out] IWSLCSession** Session);
832 +}
833 +
834 +cpp_quote("#define WSLC_E_BASE (0x0600)")
835 +cpp_quote("#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */")
836 +cpp_quote("#define WSLC_E_CONTAINER_PREFIX_AMBIGUOUS MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 2) /* 0x80040602 */")
837 +cpp_quote("#define WSLC_E_CONTAINER_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 3) /* 0x80040603 */")
838 +cpp_quote("#define WSLC_E_VOLUME_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 4) /* 0x80040604 */")
839 +cpp_quote("#define WSLC_E_CONTAINER_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 5) /* 0x80040605 */")
840 +cpp_quote("#define WSLC_E_CONTAINER_IS_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 6) /* 0x80040606 */")
841 +cpp_quote("#define WSLC_E_SESSION_RESERVED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 7) /* 0x80040607 */")
842 +cpp_quote("#define WSLC_E_INVALID_SESSION_NAME MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 8) /* 0x80040608 */")
843 +cpp_quote("#define WSLC_E_NETWORK_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 9) /* 0x80040609 */")
844 +cpp_quote("#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 10) /* 0x8004060A */")
src/windows/service/inc/wslservice.idl
+1
@@ -159,6 +159,7 @@ cpp_quote("const GUID CLSID_LxssUserSessionInBox = {0x4f476546, 0xb412, 0x4579,
159 cpp_quote("#ifdef __cplusplus")
160 cpp_quote("class DECLSPEC_UUID(\"a9b7a1b9-0671-405c-95f1-e0612cb4ce7e\") LxssUserSession;")
161 cpp_quote("class DECLSPEC_UUID(\"4f476546-b412-4579-b64c-123df331e3d6\") LxssUserSessionInBox;")
162 +
163 cpp_quote("#endif")
164
165 [
src/windows/service/stub/CMakeLists.txt
+2
@@ -1,6 +1,8 @@
1 set(SOURCES
2 ${CMAKE_CURRENT_BINARY_DIR}/../inc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE}/wslservice_i_${TARGET_PLATFORM}.c
3 ${CMAKE_CURRENT_BINARY_DIR}/../inc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE}/wslservice_p_${TARGET_PLATFORM}.c
4 + ${CMAKE_CURRENT_BINARY_DIR}/../inc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE}/wslc_i_${TARGET_PLATFORM}.c
5 + ${CMAKE_CURRENT_BINARY_DIR}/../inc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE}/wslc_p_${TARGET_PLATFORM}.c
6 ${CMAKE_CURRENT_BINARY_DIR}/../inc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE}/dlldata_${TARGET_PLATFORM}.c
7 ${CMAKE_CURRENT_LIST_DIR}/WslServiceProxyStub.def
8 ${CMAKE_CURRENT_LIST_DIR}/WslServiceProxyStub.rc)
src/windows/wslc/CMakeLists.txt new
+35
@@ -0,0 +1,35 @@
1 +set(WSLC_SUBDIRS arguments commands core services tasks)
2 +list(TRANSFORM WSLC_SUBDIRS PREPEND ${CMAKE_CURRENT_SOURCE_DIR}/ OUTPUT_VARIABLE WSLC_SUBDIR_PATHS)
3 +
4 +list(TRANSFORM WSLC_SUBDIR_PATHS APPEND /*.h OUTPUT_VARIABLE HEADER_PATTERNS)
5 +list(TRANSFORM WSLC_SUBDIR_PATHS APPEND /*.cpp OUTPUT_VARIABLE SOURCE_PATTERNS)
6 +file(GLOB_RECURSE HEADERS CONFIGURE_DEPENDS ${HEADER_PATTERNS})
7 +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS ${SOURCE_PATTERNS})
8 +
9 +# Object library for WSLC components.
10 +# Used to build the executable and also unit testing components.
11 +add_library(wslclib OBJECT ${SOURCES} ${HEADERS})
12 +target_include_directories(wslclib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${WSLC_SUBDIR_PATHS})
13 +
14 +target_link_libraries(wslclib
15 + ${COMMON_LINK_LIBRARIES}
16 + yaml-cpp
17 + common
18 + advapi32
19 + crypt32)
20 +
21 +
22 +target_precompile_headers(wslclib REUSE_FROM common)
23 +set_target_properties(wslclib PROPERTIES FOLDER windows)
24 +
25 +# Create wslc.exe
26 +# N.B. Linking wslclib (OBJECT library) brings both object files and
27 +# transitive dependencies. Do not also use $<TARGET_OBJECTS:wslclib>.
28 +add_executable(wslc)
29 +
30 +target_link_libraries(wslc wslclib)
31 +
32 +set_target_properties(wslc PROPERTIES FOLDER windows)
33 +
34 +# For prettier source tree browsing
35 +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES} ${HEADERS})
src/windows/wslc/README.md new
+2
@@ -0,0 +1,2 @@
1 +### WSL Container CLI
2 +This is the WSL Container CLI README
\ No newline at end of file
src/windows/wslc/arguments/Argument.cpp new
+75
@@ -0,0 +1,75 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + Argument.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the Argument class.
12 +
13 +--*/
14 +#include "Argument.h"
15 +#include "Command.h"
16 +#include "Exceptions.h"
17 +#include "ArgumentDefinitions.h"
18 +
19 +#include <algorithm>
20 +#include <iterator>
21 +#include <sstream>
22 +#include <string>
23 +
24 +using namespace wsl::shared;
25 +using namespace wsl::windows::wslc::argument;
26 +using namespace std::literals;
27 +
28 +namespace wsl::windows::wslc {
29 +using namespace wsl::windows::wslc::execution;
30 +
31 +// This is the main Argument creation method, allowing overrides of the default properties of arguments.
32 +// The ArgType has some core characteristic, such as the Kind, Name, and Alias. If these
33 +// need to be changed, it is recommended to create a new ArgType in ArgumentDefinitions.h. If the argument
34 +// just needs a different description, it can be overridden in the desc, or if you need it to be required,
35 +// or to allow multiple uses within a command, then those properties can be set using the Create
36 +// function below inside the command. In this way all arguments default to "1" use and not required, and
37 +// this can only be changed in the command's GetArguments function, so the defaults are always clear and
38 +// consistent. Visibility can also be overridden and is defaulted to "Help".
39 +Argument Argument::Create(ArgType type, std::optional<bool> required, std::optional<int> countLimit, std::optional<std::wstring> desc)
40 +{
41 + switch (type)
42 + {
43 +#define WSLC_ARG_CREATE_CASE(EnumName, Name, Alias, ArgumentKind, Desc) \
44 + case ArgType::EnumName: \
45 + return Argument{ \
46 + type, \
47 + L##Name, \
48 + Alias, \
49 + desc.has_value() ? std::move(desc.value()) : std::wstring(Desc), \
50 + ArgumentKind, \
51 + required.value_or(DefaultRequired), \
52 + countLimit.value_or(DefaultCountLimit)};
53 +
54 + WSLC_ARGUMENTS(WSLC_ARG_CREATE_CASE)
55 +#undef WSLC_ARG_CREATE_CASE
56 +
57 + default:
58 + THROW_HR(E_UNEXPECTED);
59 + }
60 +}
61 +
62 +// Retrieves the usage string of the Argument, based on its Alias and Name.
63 +// The format is "-alias,--name" or just "--name" if no alias.
64 +std::wstring Argument::GetUsageString() const
65 +{
66 + std::wostringstream strstr;
67 + if (!m_alias.empty())
68 + {
69 + strstr << WSLC_CLI_ARG_ID_CHAR << m_alias << L',';
70 + }
71 +
72 + strstr << WSLC_CLI_ARG_ID_CHAR << WSLC_CLI_ARG_ID_CHAR << m_name;
73 + return strstr.str();
74 +}
75 +} // namespace wsl::windows::wslc
src/windows/wslc/arguments/Argument.h new
+109
@@ -0,0 +1,109 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + Argument.h
8 +
9 +Abstract:
10 +
11 + Declaration of the Argument class for command-line argument handling.
12 +
13 +--*/
14 +#pragma once
15 +#include "ArgumentTypes.h"
16 +
17 +#include <string>
18 +
19 +#define WSLC_CLI_ARG_ID_CHAR L'-'
20 +#define WSLC_CLI_ARG_ID_STRING L"-"
21 +#define WSLC_CLI_ARG_SPLIT_CHAR L'='
22 +#define WSLC_CLI_HELP_ARG L"?"
23 +#define WSLC_CLI_HELP_ARG_STRING WSLC_CLI_ARG_ID_STRING WSLC_CLI_HELP_ARG
24 +#define NO_ALIAS L""
25 +#define NO_LIMIT -1
26 +
27 +using namespace wsl::windows::wslc::argument;
28 +
29 +namespace wsl::windows::wslc {
30 +// An argument to a command.
31 +struct Argument
32 +{
33 + // Default argument configuration constants
34 + static constexpr Kind DefaultKind = Kind::Flag;
35 + static constexpr bool DefaultRequired = false;
36 + static constexpr int DefaultCountLimit = 1;
37 +
38 + // Full constructor with all parameters
39 + Argument(
40 + ArgType argType,
41 + const std::wstring& name,
42 + const std::wstring& alias,
43 + const std::wstring& desc,
44 + argument::Kind kind = DefaultKind,
45 + bool required = DefaultRequired,
46 + int countLimit = DefaultCountLimit) :
47 + m_argType(argType), m_name(name), m_alias(alias), m_desc(desc), m_type(kind), m_required(required), m_countLimit(countLimit)
48 + {
49 + }
50 +
51 + Argument(const Argument&) = default;
52 + Argument& operator=(const Argument&) = default;
53 +
54 + Argument(Argument&&) = default;
55 + Argument& operator=(Argument&&) = default;
56 +
57 + // Creates an argument with optional overrides for table defaults
58 + static Argument Create(
59 + ArgType type,
60 + std::optional<bool> required = std::nullopt,
61 + std::optional<int> countLimit = std::nullopt,
62 + std::optional<std::wstring> desc = std::nullopt);
63 +
64 + // Gets the argument usage string in the format of "-alias,--name" or just "--name" if no alias.
65 + std::wstring GetUsageString() const;
66 +
67 + // Arguments are not localized, but the description is.
68 + const std::wstring& Name() const
69 + {
70 + return m_name;
71 + }
72 + const std::wstring& Alias() const
73 + {
74 + return m_alias;
75 + }
76 + const std::wstring& Description() const
77 + {
78 + return m_desc;
79 + }
80 + bool Required() const
81 + {
82 + return m_required;
83 + }
84 + ArgType Type() const
85 + {
86 + return m_argType;
87 + }
88 + Kind Kind() const
89 + {
90 + return m_type;
91 + }
92 + int Limit() const
93 + {
94 + return m_countLimit;
95 + }
96 +
97 + // Validates this argument's value in the provided args
98 + void Validate(const ArgMap& execArgs) const;
99 +
100 +private:
101 + ArgType m_argType;
102 + std::wstring m_name;
103 + std::wstring m_desc;
104 + std::wstring m_alias;
105 + bool m_required = DefaultRequired;
106 + argument::Kind m_type = DefaultKind;
107 + int m_countLimit = DefaultCountLimit;
108 +};
109 +} // namespace wsl::windows::wslc
src/windows/wslc/arguments/ArgumentDefinitions.h new
+105
@@ -0,0 +1,105 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ArgumentDefinitions.h
8 +
9 +Abstract:
10 +
11 + Declaration of the available Arguments with their base properties.
12 +
13 +--*/
14 +#pragma once
15 +
16 +// Here is where base argument types are defined, with their name, alias, kind, and default description.
17 +// The description can be overridden by commands if a particular command needs a different description but otherwise the
18 +// same argument type definition. The ArgType enum and the mapping of ArgType to data type are generated from this X-Macro, so all
19 +// arguments must be defined here to be used in the system. The arguments defined here are the basis for all commands,
20 +// but not all arguments need to be used by all commands, and additional properties of the arguments can be set in the command's
21 +// GetArguments function when creating the Argument with Argument::Create.
22 +
23 +// The Kind determines the data type:
24 +// - Kind::Flag -> bool
25 +// - Kind::Value -> std::wstring
26 +// - Kind::Positional -> std::wstring
27 +// - Kind::Forward -> std::vector<std::wstring>
28 +
29 +// No other files other than ArgumentValidation need to be changed when adding a new argument, and that is only
30 +// if you wish to add validation for the new argument or have it use existing validation.
31 +
32 +// X-Macro for defining all arguments in one place
33 +// Format: ARGUMENT(EnumName, Name, Alias, Kind, Desc)
34 +// clang-format off
35 +#define WSLC_ARGUMENTS(_) \
36 +_(All, "all", L"a", Kind::Flag, Localization::WSLCCLI_AllArgDescription()) \
37 +_(Attach, "attach", L"a", Kind::Flag, Localization::WSLCCLI_AttachArgDescription()) \
38 +_(BuildArg, "build-arg", NO_ALIAS, Kind::Value, Localization::WSLCCLI_BuildArgDescription()) \
39 +_(BuildPull, "pull", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_BuildPullArgDescription()) \
40 +_(BuildTarget, "target", NO_ALIAS, Kind::Value, Localization::WSLCCLI_BuildTargetArgDescription()) \
41 +/*_(CIDFile, "cidfile", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CIDFileArgDescription())*/ \
42 +_(Command, "command", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_CommandArgDescription()) \
43 +_(ContainerId, "container-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ContainerIdArgDescription()) \
44 +_(Force, "force", L"f", Kind::Flag, Localization::WSLCCLI_ForceArgDescription()) \
45 +_(Detach, "detach", L"d", Kind::Flag, Localization::WSLCCLI_DetachArgDescription()) \
46 +_(DNS, "dns", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSArgDescription()) \
47 +/*_(DNSDomain, "dns-domain", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSDomainArgDescription())*/ \
48 +_(DNSOption, "dns-option", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSOptionArgDescription()) \
49 +_(DNSSearch, "dns-search", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSSearchArgDescription()) \
50 +_(Domainname, "domainname", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DomainnameArgDescription()) \
51 +_(Driver, "driver", L"d", Kind::Value, Localization::WSLCCLI_DriverArgDescription("guest")) \
52 +_(Entrypoint, "entrypoint", NO_ALIAS, Kind::Value, Localization::WSLCCLI_EntrypointArgDescription()) \
53 +_(Env, "env", L"e", Kind::Value, Localization::WSLCCLI_EnvArgDescription()) \
54 +_(EnvFile, "env-file", NO_ALIAS, Kind::Value, Localization::WSLCCLI_EnvFileArgDescription()) \
55 +_(File, "file", L"f", Kind::Value, Localization::WSLCCLI_FileArgDescription()) \
56 +_(Follow, "follow", L"f", Kind::Flag, Localization::WSLCCLI_FollowArgDescription()) \
57 +_(Format, "format", NO_ALIAS, Kind::Value, Localization::WSLCCLI_FormatArgDescription()) \
58 +_(ForwardArgs, "arguments", NO_ALIAS, Kind::Forward, Localization::WSLCCLI_ForwardArgsDescription()) \
59 +/*_(GroupId, "groupid", NO_ALIAS, Kind::Value, Localization::WSLCCLI_GroupIdArgDescription())*/ \
60 +_(Help, "help", WSLC_CLI_HELP_ARG, Kind::Flag, Localization::WSLCCLI_HelpArgDescription()) \
61 +_(Hostname, "hostname", L"h", Kind::Value, Localization::WSLCCLI_HostnameArgDescription()) \
62 +_(ImageForce, "force", L"f", Kind::Flag, Localization::WSLCCLI_ImageForceArgDescription()) \
63 +_(ImageId, "image", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ImageIdArgDescription()) \
64 +_(Input, "input", L"i", Kind::Value, Localization::WSLCCLI_InputArgDescription()) \
65 +_(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \
66 +_(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \
67 +_(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
68 +/*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
69 +_(NoCache, "no-cache", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoCacheArgDescription()) \
70 +_(NoPrune, "no-prune", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoPruneArgDescription()) \
71 +_(NoTrunc, "no-trunc", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoTruncArgDescription()) \
72 +_(ObjectId, "object-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ObjectIdArgDescription()) \
73 +_(Options, "opt", L"o", Kind::Value, Localization::WSLCCLI_OptionsArgDescription()) \
74 +_(Output, "output", L"o", Kind::Value, Localization::WSLCCLI_OutputArgDescription()) \
75 +_(Password, "password", L"p", Kind::Value, Localization::WSLCCLI_LoginPasswordArgDescription()) \
76 +_(PasswordStdin, "password-stdin", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_LoginPasswordStdinArgDescription()) \
77 +_(Path, "path", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_PathArgDescription()) \
78 +/*_(Progress, "progress", NO_ALIAS, Kind::Value, Localization::WSLCCLI_ProgressArgDescription())*/ \
79 +_(Publish, "publish", L"p", Kind::Value, Localization::WSLCCLI_PublishArgDescription()) \
80 +_(PublishAll, "publish-all", L"P", Kind::Flag, Localization::WSLCCLI_PublishAllArgDescription()) \
81 +/*_(Pull, "pull", NO_ALIAS, Kind::Value, Localization::WSLCCLI_PullArgDescription())*/ \
82 +_(Quiet, "quiet", L"q", Kind::Flag, Localization::WSLCCLI_QuietArgDescription()) \
83 +_(Remove, "rm", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_RemoveArgDescription()) \
84 +/*_(Scheme, "scheme", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SchemeArgDescription())*/ \
85 +_(Server, "server", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_LoginServerArgDescription()) \
86 +_(Session, "session", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SessionIdArgDescription()) \
87 +_(SessionId, "session-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SessionIdPositionalArgDescription()) \
88 +_(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \
89 +_(Signal, "signal", L"s", Kind::Value, Localization::WSLCCLI_SignalArgDescription(L"SIGKILL")) \
90 +_(Source, "source", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SourceArgDescription()) \
91 +_(Tag, "tag", L"t", Kind::Value, Localization::WSLCCLI_TagArgDescription()) \
92 +_(Target, "target", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_TargetArgDescription()) \
93 +_(Time, "time", L"t", Kind::Value, Localization::WSLCCLI_TimeArgDescription()) \
94 +_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription()) \
95 +_(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \
96 +_(Type, "type", L"t", Kind::Value, Localization::WSLCCLI_TypeArgDescription()) \
97 +_(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \
98 +_(Username, "username", L"u", Kind::Value, Localization::WSLCCLI_LoginUsernameArgDescription()) \
99 +_(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
100 +_(Version, "version", L"v", Kind::Flag, Localization::WSLCCLI_VersionArgDescription()) \
101 +/*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, Localization::WSLCCLI_VirtualArgDescription())*/ \
102 +_(Volume, "volume", L"v", Kind::Value, Localization::WSLCCLI_VolumeArgDescription()) \
103 +_(VolumeName, "volume-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_VolumeNameArgDescription()) \
104 +_(WorkDir, "workdir", L"w", Kind::Value, Localization::WSLCCLI_WorkingDirArgDescription()) \
105 +// clang-format on
src/windows/wslc/arguments/ArgumentParser.cpp new
+397
@@ -0,0 +1,397 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ArgumentParser.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the ArgumentParser class.
12 +
13 +--*/
14 +#include "ArgumentParser.h"
15 +#include "Localization.h"
16 +
17 +using namespace wsl::shared;
18 +
19 +namespace wsl::windows::wslc {
20 +ParseArgumentsStateMachine::ParseArgumentsStateMachine(Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments) :
21 + m_invocation(inv), m_executionArgs(execArgs), m_arguments(std::move(arguments)), m_invocationItr(m_invocation.begin())
22 +{
23 + // Create sublists by Kind for easier processing in the state machine.
24 + for (const auto& arg : m_arguments)
25 + {
26 + switch (arg.Kind())
27 + {
28 + case Kind::Value:
29 + m_standardArgs.emplace_back(arg);
30 + break;
31 + case Kind::Flag:
32 + m_standardArgs.emplace_back(arg);
33 + break;
34 + case Kind::Positional:
35 + m_positionalArgs.emplace_back(arg);
36 + break;
37 + case Kind::Forward:
38 + m_forwardArgs.emplace_back(arg);
39 + break;
40 + }
41 + }
42 +
43 + m_positionalSearchItr = m_positionalArgs.begin();
44 +}
45 +
46 +bool ParseArgumentsStateMachine::Step()
47 +{
48 + if (m_invocationItr == m_invocation.end())
49 + {
50 + return false;
51 + }
52 +
53 + m_state = StepInternal();
54 + return true;
55 +}
56 +
57 +void ParseArgumentsStateMachine::ThrowIfError() const
58 +{
59 + if (m_state.Exception())
60 + {
61 + throw m_state.Exception().value();
62 + }
63 + // If the next argument was to be a value, but none was provided, convert it to an exception.
64 + else if (m_state.Type() && m_invocationItr == m_invocation.end())
65 + {
66 + throw ArgumentException(Localization::WSLCCLI_MissingArgumentError(m_state.Arg()));
67 + }
68 +}
69 +
70 +const Argument* ParseArgumentsStateMachine::NextPositional()
71 +{
72 + // Find the next appropriate positional arg if the current itr isn't one or has hit its limit.
73 + while (m_positionalSearchItr != m_positionalArgs.end() &&
74 + (m_executionArgs.Count(m_positionalSearchItr->Type()) == m_positionalSearchItr->Limit()))
75 + {
76 + ++m_positionalSearchItr;
77 + }
78 +
79 + if (m_positionalSearchItr == m_positionalArgs.end())
80 + {
81 + return nullptr;
82 + }
83 +
84 + return &*m_positionalSearchItr;
85 +}
86 +
87 +// Parse arguments as such:
88 +// 1. If argument starts with a single -, the alias is considered (can be 1-2 characters).
89 +// a. If the named argument alias (a or ab) needs a VALUE, it can be provided in these ways:
90 +// -a=VALUE or -ab=VALUE
91 +// -a VALUE or -ab VALUE
92 +// b. If the argument is a flag, additional characters after are treated as if they start
93 +// with a -, repeatedly until the end of the argument is reached. Fails if non-flags hit.
94 +// 2. If the argument starts with a double --, only the full name is considered.
95 +// a. If the named argument (arg) needs a VALUE, it can be provided in these ways:
96 +// --arg=VALUE
97 +// --arg VALUE
98 +// 3. If the argument does not start with any -, it is considered the next positional argument.
99 +// 4. Once a positional argument is encountered, all subsequent arguments are considered positional
100 +// 5. If the command only has 1 positional argument, all subsequent arguments are considered forwarded.
101 +ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal()
102 +{
103 + // Get the next argument from the invocation.
104 + auto currArg = std::wstring_view{*m_invocationItr};
105 + ++m_invocationItr;
106 +
107 + // If current state has a type, then that means this must be a value for the previous argument.
108 + if (m_state.Type())
109 + {
110 + m_executionArgs.Add(m_state.Type().value(), std::wstring{currArg});
111 + return {};
112 + }
113 +
114 + // If this command has forwarded args present and we have found a positional argument,
115 + // the all remaining args are considered positional or forwarded.
116 + if (!m_forwardArgs.empty() && m_anchorPositional.has_value())
117 + {
118 + return ProcessAnchoredPositionals(currArg);
119 + }
120 +
121 + // Arg does not begin with '-' so it is neither an alias nor a named value, must be positional.
122 + if (currArg.empty() || currArg[0] != WSLC_CLI_ARG_ID_CHAR)
123 + {
124 + return ProcessPositionalArgument(currArg);
125 + }
126 +
127 + // The currentArg is non-empty, and starts with a -.
128 + if (currArg.length() == 1)
129 + {
130 + // If it is only one character, then it is an error since it is neither an alias nor a named argument.
131 + return ArgumentException(Localization::WSLCCLI_InvalidArgumentSpecifierError(currArg));
132 + }
133 +
134 + // Single '-' that is 2 characters or more means this must be an alias or collection of alias flags.
135 + if (currArg[1] != WSLC_CLI_ARG_ID_CHAR)
136 + {
137 + return ProcessAliasArgument(currArg);
138 + }
139 +
140 + // The currentArg must be a named argument.
141 + return ProcessNamedArgument(currArg);
142 +}
143 +
144 +// Assumes non-empty and does not begin with '-'.
145 +ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessPositionalArgument(const std::wstring_view& currArg)
146 +{
147 + WI_ASSERT(!currArg.empty() && currArg[0] != WSLC_CLI_ARG_ID_CHAR);
148 +
149 + const Argument* nextPositional = NextPositional();
150 + if (!nextPositional)
151 + {
152 + return ArgumentException(Localization::WSLCCLI_ExtraPositionalError(currArg));
153 + }
154 +
155 + // First positional found is the anchor positional.
156 + if (!m_anchorPositional.has_value())
157 + {
158 + m_anchorPositional = Argument(*nextPositional);
159 + }
160 +
161 + m_executionArgs.Add(nextPositional->Type(), std::wstring{currArg});
162 + return {};
163 +}
164 +
165 +// Assumes one positional has already been found and therefore there are no remaining Kind Value/Flag arguments.
166 +// Only Kind::Positional or Kind::Forward arguments should remain.
167 +ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAnchoredPositionals(const std::wstring_view& currArg)
168 +{
169 + WI_ASSERT(m_anchorPositional.has_value());
170 +
171 + // If we haven't reached the limit for the anchor positional, treat this as another anchor positional.
172 + // Anchors with NO_LIMIT will never be full and therefore will always treat subsequent positionals as anchors.
173 + if ((m_executionArgs.Count(m_anchorPositional.value().Type()) < m_anchorPositional.value().Limit()) ||
174 + (m_anchorPositional.value().Limit() == NO_LIMIT))
175 + {
176 + // Validate that we don't have any invalid argument specifiers.
177 + // Anchor positionals with multiple values should be order-independent, which means a
178 + // '-' at the start of the first one would be invalid, so it should also be invalid for
179 + // all other anchor positionals of the same type.
180 + if (!currArg.empty() && currArg[0] == WSLC_CLI_ARG_ID_CHAR)
181 + {
182 + return ArgumentException(Localization::WSLCCLI_InvalidArgumentSpecifierError(currArg));
183 + }
184 +
185 + m_executionArgs.Add(m_anchorPositional.value().Type(), std::wstring{currArg});
186 + return {};
187 + }
188 +
189 + // There are three possibilities for this argument:
190 + // 1) It is another positional argument (ex: run <imagename> <command>)
191 + // 2) It is a forwarded argument set that could be anything (most likely)
192 + // 3) It is an input error and there should be no such argument.
193 +
194 + // Check next positional.
195 + const Argument* nextPositional = NextPositional();
196 + if (nextPositional)
197 + {
198 + m_executionArgs.Add(nextPositional->Type(), std::wstring{currArg});
199 + return {};
200 + }
201 +
202 + // Handle case where we expect a positional but don't find one - check forwarded args.
203 +
204 + // Check for forwarded arg existence.
205 + if (m_forwardArgs.empty())
206 + {
207 + return ArgumentException(Localization::WSLCCLI_CommandHasNoForwardArgumentsError(currArg));
208 + }
209 +
210 + // currArg is the first forwarded argument
211 + // All the rest of the args are forward args.
212 + std::vector<std::wstring> forwardedArgs;
213 + forwardedArgs.emplace_back(std::wstring{currArg});
214 + while (m_invocationItr != m_invocation.end())
215 + {
216 + forwardedArgs.emplace_back(std::wstring{*m_invocationItr});
217 + ++m_invocationItr;
218 + }
219 +
220 + m_executionArgs.Add(m_forwardArgs.front().Type(), std::move(forwardedArgs));
221 + return {};
222 +}
223 +
224 +// Assumes argument begins with '-' and is at least 2 characters.
225 +ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgument(const std::wstring_view& currArg)
226 +{
227 + WI_ASSERT(currArg.length() >= 2 && currArg[0] == WSLC_CLI_ARG_ID_CHAR && currArg[1] != WSLC_CLI_ARG_ID_CHAR);
228 +
229 + // This may be a collection of boolean alias flags.
230 + // Helper to find an argument by alias starting at a specific position.
231 + auto findArgumentByAlias = [this](const std::wstring_view& str, size_t startPos, size_t& aliasLength) -> const Argument* {
232 + for (const auto& arg : m_standardArgs)
233 + {
234 + const auto& alias = arg.Alias();
235 + if (alias.empty())
236 + {
237 + continue;
238 + }
239 +
240 + if (startPos + alias.length() <= str.length() && str.compare(startPos, alias.length(), alias) == 0)
241 + {
242 + aliasLength = alias.length();
243 + return &arg;
244 + }
245 + }
246 +
247 + return nullptr;
248 + };
249 +
250 + // Find the first alias starting at position 1 (after the '-')
251 + size_t aliasLength = 0;
252 + const Argument* firstArg = findArgumentByAlias(currArg, 1, aliasLength);
253 + if (!firstArg)
254 + {
255 + return ArgumentException(Localization::WSLCCLI_InvalidAliasError(currArg));
256 + }
257 +
258 + // Position after the first alias
259 + size_t currentPos = 1 + aliasLength;
260 +
261 + // Check if this argument expects a value
262 + if (firstArg->Kind() == Kind::Value)
263 + {
264 + // Kind::Value is only allowed if it's the last flag (no more characters after it, or '=' follows)
265 + if (currentPos >= currArg.length())
266 + {
267 + // No more characters - value should be in next argument
268 + return {firstArg->Type(), currArg};
269 + }
270 +
271 + if (currArg[currentPos] != WSLC_CLI_ARG_SPLIT_CHAR)
272 + {
273 + // There are more characters but it's not '=' - this is invalid
274 + return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg));
275 + }
276 +
277 + // Value is adjoined after '='
278 + ProcessAdjoinedValue(firstArg->Type(), currArg.substr(currentPos + 1));
279 + return {};
280 + }
281 +
282 + // Boolean flag - add it and process any adjoined flags
283 + m_executionArgs.Add(firstArg->Type(), true);
284 +
285 + // Process remaining adjoined flags
286 + while (currentPos < currArg.length())
287 + {
288 + const Argument* nextArg = findArgumentByAlias(currArg, currentPos, aliasLength);
289 +
290 + if (!nextArg)
291 + {
292 + return ArgumentException(Localization::WSLCCLI_AdjoinedNotFoundError(currArg));
293 + }
294 +
295 + // Update position before checking Kind
296 + size_t nextPos = currentPos + aliasLength;
297 +
298 + if (nextArg->Kind() == Kind::Value)
299 + {
300 + // Kind::Value is only allowed if it's the last flag
301 + if (nextPos >= currArg.length())
302 + {
303 + // No more characters - value should be in next argument
304 + return {nextArg->Type(), currArg};
305 + }
306 +
307 + if (currArg[nextPos] != WSLC_CLI_ARG_SPLIT_CHAR)
308 + {
309 + // There are more characters but it's not '=' - this is invalid
310 + return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg));
311 + }
312 +
313 + // Value is adjoined after '='
314 + ProcessAdjoinedValue(nextArg->Type(), currArg.substr(nextPos + 1));
315 + return {};
316 + }
317 +
318 + m_executionArgs.Add(nextArg->Type(), true);
319 + currentPos = nextPos;
320 + }
321 +
322 + return {};
323 +}
324 +
325 +// Assumes the arg value begins with -- and is at least 2 characters long.
326 +ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgument(const std::wstring_view& currArg)
327 +{
328 + WI_ASSERT(currArg.starts_with(L"--"));
329 +
330 + if (currArg.length() == 2)
331 + {
332 + // Missing argument name after double dash, this is an error.
333 + return ArgumentException(Localization::WSLCCLI_MissingArgumentNameError(currArg));
334 + }
335 +
336 + // This is an arg name, find it and process its value if needed.
337 + // Skip the double arg identifier chars.
338 + size_t argStart = currArg.find_first_not_of(WSLC_CLI_ARG_ID_CHAR);
339 + std::wstring_view argName = currArg.substr(argStart);
340 + bool argFound = false;
341 +
342 + bool hasAdjoinedValue = false;
343 + std::wstring_view argValue;
344 + size_t splitChar = argName.find_first_of(WSLC_CLI_ARG_SPLIT_CHAR);
345 + if (splitChar != std::string::npos)
346 + {
347 + // There is an '=' in this arg, it has an adjoined value, split it out.
348 + hasAdjoinedValue = true;
349 + argValue = argName.substr(splitChar + 1);
350 + argName = argName.substr(0, splitChar);
351 + }
352 +
353 + // Find a matching standard arg with this name.
354 + for (const auto& arg : m_standardArgs)
355 + {
356 + if (string::IsEqual(argName, arg.Name()))
357 + {
358 + // Found a match, process by kind.
359 + if (arg.Kind() == Kind::Flag)
360 + {
361 + // TODO: Consider supporting --flag and --flag=true or --flag=false for bool args.
362 + if (hasAdjoinedValue)
363 + {
364 + return ArgumentException(Localization::WSLCCLI_FlagContainAdjoinedError(currArg));
365 + }
366 +
367 + m_executionArgs.Add(arg.Type(), true);
368 + return {};
369 + }
370 +
371 + // Not a Flag, must be a Value, and therefore must have a value provided.
372 + if (hasAdjoinedValue)
373 + {
374 + ProcessAdjoinedValue(arg.Type(), argValue);
375 + return {};
376 + }
377 +
378 + // The value should be the next argument.
379 + return {arg.Type(), currArg};
380 + }
381 + }
382 +
383 + // We found no matching argument for this name, this is an invalid argument name.
384 + return ArgumentException(Localization::WSLCCLI_InvalidNameError(currArg));
385 +}
386 +
387 +void ParseArgumentsStateMachine::ProcessAdjoinedValue(ArgType type, std::wstring_view value)
388 +{
389 + // If the adjoined value is wrapped in quotes, strip them off.
390 + if (value.length() >= 2 && value[0] == '"' && value[value.length() - 1] == '"')
391 + {
392 + value = value.substr(1, value.length() - 2);
393 + }
394 +
395 + m_executionArgs.Add(type, std::wstring{value});
396 +}
397 +} // namespace wsl::windows::wslc
src/windows/wslc/arguments/ArgumentParser.h new
+122
@@ -0,0 +1,122 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ArgumentParser.h
8 +
9 +Abstract:
10 +
11 + Declaration of the ArgumentParser class for command-line argument parsing.
12 +
13 +--*/
14 +#pragma once
15 +#include "Argument.h"
16 +#include "Exceptions.h"
17 +#include "Invocation.h"
18 +#include "ArgumentTypes.h"
19 +
20 +#include <optional>
21 +#include <string>
22 +#include <string_view>
23 +#include <vector>
24 +#include <type_traits>
25 +
26 +namespace wsl::windows::wslc {
27 +// The argument parsing state machine.
28 +// It is broken out to enable completion to process arguments, ignore errors,
29 +// and determine the likely state of the word to be completed.
30 +struct ParseArgumentsStateMachine
31 +{
32 + ParseArgumentsStateMachine(Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments);
33 +
34 + ParseArgumentsStateMachine(const ParseArgumentsStateMachine&) = delete;
35 + ParseArgumentsStateMachine& operator=(const ParseArgumentsStateMachine&) = delete;
36 +
37 + ParseArgumentsStateMachine(ParseArgumentsStateMachine&&) = default;
38 + ParseArgumentsStateMachine& operator=(ParseArgumentsStateMachine&&) = default;
39 +
40 + // Processes the next argument from the invocation.
41 + // Returns true if there was an argument to process;
42 + // returns false if there were none.
43 + bool Step();
44 +
45 + // Throws if there was an error during the prior step.
46 + void ThrowIfError() const;
47 +
48 + // The current state of the state machine.
49 + // An empty state indicates that the next argument can be anything.
50 + struct State
51 + {
52 + State() = default;
53 + State(ArgType type, std::wstring_view arg) : m_type(type), m_arg(arg)
54 + {
55 + }
56 + State(ArgumentException ce) : m_exception(std::move(ce))
57 + {
58 + }
59 +
60 + // If set, indicates that the next argument is a value for this type.
61 + const std::optional<ArgType>& Type() const
62 + {
63 + return m_type;
64 + }
65 +
66 + // The actual argument string associated with Type.
67 + const std::wstring& Arg() const
68 + {
69 + return m_arg;
70 + }
71 +
72 + // If set, indicates that the last argument produced an error.
73 + const std::optional<ArgumentException>& Exception() const
74 + {
75 + return m_exception;
76 + }
77 +
78 + private:
79 + std::optional<ArgType> m_type;
80 + std::wstring m_arg;
81 + std::optional<ArgumentException> m_exception;
82 + };
83 +
84 + const State& GetState() const
85 + {
86 + return m_state;
87 + }
88 +
89 + // Gets the next positional argument, or nullptr if there is not one.
90 + const Argument* NextPositional();
91 +
92 + const std::vector<Argument>& Arguments() const
93 + {
94 + return m_arguments;
95 + }
96 +
97 +private:
98 + State StepInternal();
99 + State ProcessPositionalArgument(const std::wstring_view& currArg);
100 + State ProcessAnchoredPositionals(const std::wstring_view& currArg);
101 + State ProcessAliasArgument(const std::wstring_view& currArg);
102 + State ProcessNamedArgument(const std::wstring_view& currArg);
103 + void ProcessAdjoinedValue(ArgType type, std::wstring_view value);
104 +
105 + Invocation& m_invocation;
106 + ArgMap& m_executionArgs;
107 + std::vector<Argument> m_arguments;
108 +
109 + Invocation::iterator m_invocationItr;
110 + std::vector<Argument>::iterator m_positionalSearchItr;
111 +
112 + // The anchor positional is the first positional argument processed.
113 + std::optional<Argument> m_anchorPositional = std::nullopt;
114 +
115 + // Separate arguments by Kind
116 + std::vector<Argument> m_standardArgs = {};
117 + std::vector<Argument> m_positionalArgs = {};
118 + std::vector<Argument> m_forwardArgs = {};
119 +
120 + State m_state;
121 +};
122 +} // namespace wsl::windows::wslc
src/windows/wslc/arguments/ArgumentTypes.h new
+103
@@ -0,0 +1,103 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ArgumentTypes.h
8 +
9 +Abstract:
10 +
11 + Declaration of the ArgumentTypes, which includes all ArgTypes and their properties.
12 +
13 +--*/
14 +#pragma once
15 +#include "ArgumentDefinitions.h"
16 +#include "EnumVariantMap.h"
17 +#include <string>
18 +#include <vector>
19 +#include <array>
20 +#include <type_traits>
21 +
22 +namespace wsl::windows::wslc::argument {
23 +// General format: commandname [Flag | Value]* [Positional]* [Forward]
24 +// Argument Kind, which determines both parsing behavior and data type.
25 +enum class Kind
26 +{
27 + // Boolean flag argument (--flag or -f). Data type: bool
28 + Flag,
29 +
30 + // String value argument (--option value or -o value). Data type: std::wstring
31 + Value,
32 +
33 + // Positional argument (implied by position, no flag). Data type: std::wstring
34 + Positional,
35 +
36 + // Forward arguments (remaining args passed through). Data type: std::wstring
37 + Forward,
38 +};
39 +
40 +// Generate ArgType enum from X-macro
41 +enum class ArgType : size_t
42 +{
43 +#define WSLC_ARG_ENUM(EnumName, Name, Alias, Kind, Desc) EnumName,
44 + WSLC_ARGUMENTS(WSLC_ARG_ENUM)
45 +#undef WSLC_ARG_ENUM
46 +
47 + // This should always be at the end
48 + Max,
49 +};
50 +
51 +namespace details {
52 + // Map Kind to data type
53 + template <Kind K>
54 + struct KindToType;
55 +
56 + template <>
57 + struct KindToType<Kind::Flag>
58 + {
59 + using type = bool;
60 + };
61 +
62 + template <>
63 + struct KindToType<Kind::Value>
64 + {
65 + using type = std::wstring;
66 + };
67 +
68 + template <>
69 + struct KindToType<Kind::Positional>
70 + {
71 + using type = std::wstring;
72 + };
73 +
74 + template <>
75 + struct KindToType<Kind::Forward>
76 + {
77 + using type = std::vector<std::wstring>;
78 + };
79 +
80 + template <ArgType D>
81 + struct ArgDataMapping
82 + {
83 + };
84 +
85 + // Generate data mappings from X-macro - Kind determines the type
86 +#define WSLC_ARG_MAPPING(EnumName, Name, Alias, ArgumentKind, Desc) \
87 + template <> \
88 + struct ArgDataMapping<ArgType::EnumName> \
89 + { \
90 + using value_t = typename KindToType<ArgumentKind>::type; \
91 + };
92 +
93 + WSLC_ARGUMENTS(WSLC_ARG_MAPPING)
94 +#undef WSLC_ARG_MAPPING
95 +
96 +} // namespace details
97 +
98 +// This is the main ArgType map used for storing parsed arguments.
99 +struct ArgMap : wsl::windows::wslc::EnumBasedVariantMap<ArgType, wsl::windows::wslc::argument::details::ArgDataMapping>
100 +{
101 +};
102 +
103 +} // namespace wsl::windows::wslc::argument
src/windows/wslc/arguments/ArgumentValidation.cpp new
+194
@@ -0,0 +1,194 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ArgumentValidation.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the Argument Validation.
12 +
13 +--*/
14 +#include "Argument.h"
15 +#include "ArgumentTypes.h"
16 +#include "ArgumentValidation.h"
17 +#include "ContainerModel.h"
18 +#include "Exceptions.h"
19 +#include "Localization.h"
20 +#include <charconv>
21 +#include <format>
22 +#include <unordered_map>
23 +#include <wslc.h>
24 +
25 +using namespace wsl::windows::common;
26 +using namespace wsl::shared;
27 +using namespace wsl::shared::string;
28 +
29 +namespace wsl::windows::wslc {
30 +// Common argument validation that occurs across multiple commands.
31 +void Argument::Validate(const ArgMap& execArgs) const
32 +{
33 + switch (m_argType)
34 + {
35 + case ArgType::Format:
36 + validation::ValidateFormatTypeFromString(execArgs.GetAll<ArgType::Format>(), m_name);
37 + break;
38 +
39 + case ArgType::Signal:
40 + validation::ValidateWSLCSignalFromString(execArgs.GetAll<ArgType::Signal>(), m_name);
41 + break;
42 +
43 + case ArgType::Time:
44 + validation::ValidateIntegerFromString<LONGLONG>(execArgs.GetAll<ArgType::Time>(), m_name);
45 + break;
46 +
47 + case ArgType::Volume:
48 + validation::ValidateVolumeMount(execArgs.GetAll<ArgType::Volume>());
49 + break;
50 +
51 + case ArgType::WorkDir:
52 + {
53 + const auto& value = execArgs.Get<ArgType::WorkDir>();
54 + if (value.empty() ||
55 + std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
56 + {
57 + throw ArgumentException(std::format(L"Invalid {} argument value: working directory cannot be empty or whitespace", m_name));
58 + }
59 + break;
60 + }
61 +
62 + default:
63 + break;
64 + }
65 +}
66 +} // namespace wsl::windows::wslc
67 +
68 +namespace wsl::windows::wslc::validation {
69 +
70 +// Map of signal names to WSLCSignal enum values
71 +static const std::unordered_map<std::wstring, WSLCSignal> SignalMap = {
72 + {L"SIGHUP", WSLCSignalSIGHUP}, {L"SIGINT", WSLCSignalSIGINT}, {L"SIGQUIT", WSLCSignalSIGQUIT},
73 + {L"SIGILL", WSLCSignalSIGILL}, {L"SIGTRAP", WSLCSignalSIGTRAP}, {L"SIGABRT", WSLCSignalSIGABRT},
74 + {L"SIGIOT", WSLCSignalSIGIOT}, {L"SIGBUS", WSLCSignalSIGBUS}, {L"SIGFPE", WSLCSignalSIGFPE},
75 + {L"SIGKILL", WSLCSignalSIGKILL}, {L"SIGUSR1", WSLCSignalSIGUSR1}, {L"SIGSEGV", WSLCSignalSIGSEGV},
76 + {L"SIGUSR2", WSLCSignalSIGUSR2}, {L"SIGPIPE", WSLCSignalSIGPIPE}, {L"SIGALRM", WSLCSignalSIGALRM},
77 + {L"SIGTERM", WSLCSignalSIGTERM}, {L"SIGTKFLT", WSLCSignalSIGTKFLT}, {L"SIGCHLD", WSLCSignalSIGCHLD},
78 + {L"SIGCONT", WSLCSignalSIGCONT}, {L"SIGSTOP", WSLCSignalSIGSTOP}, {L"SIGTSTP", WSLCSignalSIGTSTP},
79 + {L"SIGTTIN", WSLCSignalSIGTTIN}, {L"SIGTTOU", WSLCSignalSIGTTOU}, {L"SIGURG", WSLCSignalSIGURG},
80 + {L"SIGXCPU", WSLCSignalSIGXCPU}, {L"SIGXFSZ", WSLCSignalSIGXFSZ}, {L"SIGVTALRM", WSLCSignalSIGVTALRM},
81 + {L"SIGPROF", WSLCSignalSIGPROF}, {L"SIGWINCH", WSLCSignalSIGWINCH}, {L"SIGIO", WSLCSignalSIGIO},
82 + {L"SIGPOLL", WSLCSignalSIGPOLL}, {L"SIGPWR", WSLCSignalSIGPWR}, {L"SIGSYS", WSLCSignalSIGSYS},
83 +};
84 +
85 +void ValidateWSLCSignalFromString(const std::vector<std::wstring>& values, const std::wstring& argName)
86 +{
87 + for (const auto& value : values)
88 + {
89 + std::ignore = GetWSLCSignalFromString(value, argName);
90 + }
91 +}
92 +
93 +void ValidateVolumeMount(const std::vector<std::wstring>& values)
94 +{
95 + for (const auto& value : values)
96 + {
97 + std::ignore = models::VolumeMount::Parse(value);
98 + }
99 +}
100 +
101 +// Convert string to WSLCSignal enum - accepts either signal name (e.g., "SIGKILL") or number (e.g., "9")
102 +WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName)
103 +{
104 + constexpr int MIN_SIGNAL = WSLCSignalSIGHUP;
105 + constexpr int MAX_SIGNAL = WSLCSignalSIGSYS;
106 + constexpr std::wstring_view sigPrefix = L"SIG";
107 +
108 + // Normalize input: ensure it has "SIG" prefix for map lookup
109 + std::wstring normalizedInput;
110 + if (IsEqual(input.substr(0, sigPrefix.size()), sigPrefix, true))
111 + {
112 + normalizedInput = input;
113 + }
114 + else
115 + {
116 + normalizedInput = std::wstring(sigPrefix) + input;
117 + }
118 +
119 + for (const auto& [signalName, signalValue] : SignalMap)
120 + {
121 + if (IsEqual(normalizedInput, signalName, true))
122 + {
123 + return signalValue;
124 + }
125 + }
126 +
127 + // User may have input an integer representation instead.
128 + int signalValue{};
129 + try
130 + {
131 + signalValue = GetIntegerFromString<int>(input, argName);
132 + }
133 + // If it fails to be converted give a better user message than just the integer conversion
134 + // failure since we also know it failed to be found in the map.
135 + catch (ArgumentException)
136 + {
137 + throw ArgumentException(Localization::WSLCCLI_InvalidSignalError(argName, input));
138 + }
139 +
140 + if (signalValue < MIN_SIGNAL || signalValue > MAX_SIGNAL)
141 + {
142 + throw ArgumentException(Localization::WSLCCLI_SignalOutOfRangeError(argName, input, MIN_SIGNAL, MAX_SIGNAL));
143 + }
144 +
145 + return static_cast<WSLCSignal>(signalValue);
146 +}
147 +
148 +void ValidateFormatTypeFromString(const std::vector<std::wstring>& values, const std::wstring& argName)
149 +{
150 + for (const auto& value : values)
151 + {
152 + std::ignore = GetFormatTypeFromString(value, argName);
153 + }
154 +}
155 +
156 +FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName)
157 +{
158 + if (IsEqual(input, L"json"))
159 + {
160 + return FormatType::Json;
161 + }
162 + else if (IsEqual(input, L"table"))
163 + {
164 + return FormatType::Table;
165 + }
166 + else
167 + {
168 + throw ArgumentException(std::format(
169 + L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input));
170 + }
171 +}
172 +
173 +InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName)
174 +{
175 + if (IsEqual(input, L"image"))
176 + {
177 + return InspectType::Image;
178 + }
179 + else if (IsEqual(input, L"container"))
180 + {
181 + return InspectType::Container;
182 + }
183 + else if (IsEqual(input, L"volume"))
184 + {
185 + return InspectType::Volume;
186 + }
187 + else
188 + {
189 + constexpr std::wstring_view supportedValues = L"image, container, volume";
190 + throw ArgumentException(Localization::WSLCCLI_InvalidInspectError(argName, input, supportedValues));
191 + }
192 +}
193 +
194 +} // namespace wsl::windows::wslc::validation
src/windows/wslc/arguments/ArgumentValidation.h new
+68
@@ -0,0 +1,68 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ArgumentValidation.h
8 +
9 +Abstract:
10 +
11 + Declaration of argument validation functions.
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include "Exceptions.h"
17 +#include "ContainerModel.h"
18 +#include "InspectModel.h"
19 +#include <string>
20 +#include <vector>
21 +#include <charconv>
22 +#include <format>
23 +#include <wslc.h>
24 +#include <string.hpp>
25 +
26 +using namespace wsl::windows::wslc::models;
27 +
28 +namespace wsl::windows::wslc::validation {
29 +
30 +template <typename T>
31 +void ValidateIntegerFromString(const std::vector<std::wstring>& values, const std::wstring& argName)
32 +{
33 + for (const auto& value : values)
34 + {
35 + std::ignore = GetIntegerFromString<T>(value, argName);
36 + }
37 +}
38 +
39 +template <typename T>
40 +T GetIntegerFromString(const std::wstring& value, const std::wstring& argName = {})
41 +{
42 + std::string narrowValue = wsl::windows::common::string::WideToMultiByte(value);
43 +
44 + T convertedValue{};
45 + const char* begin = narrowValue.c_str();
46 + const char* end = begin + narrowValue.size();
47 + auto result = std::from_chars(begin, end, convertedValue);
48 +
49 + // Reject conversion errors and partial parses (e.g. "1.5", "9abc")
50 + if (result.ec != std::errc() || result.ptr != end)
51 + {
52 + throw ArgumentException(std::format(L"Invalid {} argument value: {}", argName, value));
53 + }
54 +
55 + return convertedValue;
56 +}
57 +
58 +void ValidateWSLCSignalFromString(const std::vector<std::wstring>& values, const std::wstring& argName);
59 +WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName = {});
60 +
61 +void ValidateFormatTypeFromString(const std::vector<std::wstring>& values, const std::wstring& argName);
62 +FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName = {});
63 +
64 +InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName);
65 +
66 +void ValidateVolumeMount(const std::vector<std::wstring>& values);
67 +
68 +} // namespace wsl::windows::wslc::validation
\ No newline at end of file
src/windows/wslc/commands/ContainerAttachCommand.cpp new
+51
@@ -0,0 +1,51 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerAttachCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Attach Command
27 +std::vector<Argument> ContainerAttachCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, true),
31 + Argument::Create(ArgType::Session),
32 + };
33 +}
34 +
35 +std::wstring ContainerAttachCommand::ShortDescription() const
36 +{
37 + return Localization::WSLCCLI_ContainerAttachDesc();
38 +}
39 +
40 +std::wstring ContainerAttachCommand::LongDescription() const
41 +{
42 + return Localization::WSLCCLI_ContainerAttachLongDesc();
43 +}
44 +
45 +void ContainerAttachCommand::ExecuteInternal(CLIExecutionContext& context) const
46 +{
47 + context //
48 + << CreateSession //
49 + << AttachContainer(context.Args.Get<ArgType::ContainerId>());
50 +}
51 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/ContainerCommand.cpp new
+58
@@ -0,0 +1,58 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +#include "CLIExecutionContext.h"
15 +#include "ContainerCommand.h"
16 +
17 +using namespace wsl::windows::wslc::execution;
18 +using namespace wsl::shared;
19 +
20 +namespace wsl::windows::wslc {
21 +// Container Root Command
22 +std::vector<std::unique_ptr<Command>> ContainerCommand::GetCommands() const
23 +{
24 + std::vector<std::unique_ptr<Command>> commands;
25 + commands.push_back(std::make_unique<ContainerAttachCommand>(FullName()));
26 + commands.push_back(std::make_unique<ContainerCreateCommand>(FullName()));
27 + commands.push_back(std::make_unique<ContainerExecCommand>(FullName()));
28 + commands.push_back(std::make_unique<ContainerInspectCommand>(FullName()));
29 + commands.push_back(std::make_unique<ContainerKillCommand>(FullName()));
30 + commands.push_back(std::make_unique<ContainerLogsCommand>(FullName()));
31 + commands.push_back(std::make_unique<ContainerListCommand>(FullName()));
32 + commands.push_back(std::make_unique<ContainerRemoveCommand>(FullName()));
33 + commands.push_back(std::make_unique<ContainerRunCommand>(FullName()));
34 + commands.push_back(std::make_unique<ContainerStartCommand>(FullName()));
35 + commands.push_back(std::make_unique<ContainerStopCommand>(FullName()));
36 + return commands;
37 +}
38 +
39 +std::vector<Argument> ContainerCommand::GetArguments() const
40 +{
41 + return {};
42 +}
43 +
44 +std::wstring ContainerCommand::ShortDescription() const
45 +{
46 + return Localization::WSLCCLI_ContainerCommandDesc();
47 +}
48 +
49 +std::wstring ContainerCommand::LongDescription() const
50 +{
51 + return Localization::WSLCCLI_ContainerCommandLongDesc();
52 +}
53 +
54 +void ContainerCommand::ExecuteInternal(CLIExecutionContext& context) const
55 +{
56 + OutputHelp();
57 +}
58 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ContainerCommand.h new
+200
@@ -0,0 +1,200 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerCommand.h
8 +
9 +Abstract:
10 +
11 + Declaration of command classes and interfaces.
12 +
13 +--*/
14 +#pragma once
15 +#include "Command.h"
16 +
17 +namespace wsl::windows::wslc {
18 +// Root Container Command
19 +struct ContainerCommand final : public Command
20 +{
21 + constexpr static std::wstring_view CommandName = L"container";
22 + ContainerCommand(const std::wstring& parent) : Command(CommandName, parent)
23 + {
24 + }
25 + std::vector<Argument> GetArguments() const override;
26 + std::wstring ShortDescription() const override;
27 + std::wstring LongDescription() const override;
28 +
29 + std::vector<std::unique_ptr<Command>> GetCommands() const override;
30 +
31 +protected:
32 + void ExecuteInternal(CLIExecutionContext& context) const override;
33 +};
34 +
35 +// Attach Command
36 +struct ContainerAttachCommand final : public Command
37 +{
38 + constexpr static std::wstring_view CommandName = L"attach";
39 + ContainerAttachCommand(const std::wstring& parent) : Command(CommandName, parent)
40 + {
41 + }
42 + std::vector<Argument> GetArguments() const override;
43 + std::wstring ShortDescription() const override;
44 + std::wstring LongDescription() const override;
45 +
46 +protected:
47 + void ExecuteInternal(CLIExecutionContext& context) const override;
48 +};
49 +
50 +// Create Command
51 +struct ContainerCreateCommand final : public Command
52 +{
53 + constexpr static std::wstring_view CommandName = L"create";
54 + ContainerCreateCommand(const std::wstring& parent) : Command(CommandName, parent)
55 + {
56 + }
57 + std::vector<Argument> GetArguments() const override;
58 + std::wstring ShortDescription() const override;
59 + std::wstring LongDescription() const override;
60 +
61 +protected:
62 + void ExecuteInternal(CLIExecutionContext& context) const override;
63 +};
64 +
65 +// Exec Command
66 +struct ContainerExecCommand final : public Command
67 +{
68 + constexpr static std::wstring_view CommandName = L"exec";
69 + ContainerExecCommand(const std::wstring& parent) : Command(CommandName, parent)
70 + {
71 + }
72 + std::vector<Argument> GetArguments() const override;
73 + std::wstring ShortDescription() const override;
74 + std::wstring LongDescription() const override;
75 +
76 +protected:
77 + void ExecuteInternal(CLIExecutionContext& context) const override;
78 +};
79 +
80 +// Inspect Command
81 +struct ContainerInspectCommand final : public Command
82 +{
83 + constexpr static std::wstring_view CommandName = L"inspect";
84 + ContainerInspectCommand(const std::wstring& parent) : Command(CommandName, parent)
85 + {
86 + }
87 + std::vector<Argument> GetArguments() const override;
88 + std::wstring ShortDescription() const override;
89 + std::wstring LongDescription() const override;
90 +
91 +protected:
92 + void ExecuteInternal(CLIExecutionContext& context) const override;
93 +};
94 +
95 +// Kill Command
96 +struct ContainerKillCommand final : public Command
97 +{
98 + constexpr static std::wstring_view CommandName = L"kill";
99 + ContainerKillCommand(const std::wstring& parent) : Command(CommandName, parent)
100 + {
101 + }
102 + std::vector<Argument> GetArguments() const override;
103 + std::wstring ShortDescription() const override;
104 + std::wstring LongDescription() const override;
105 +
106 +protected:
107 + void ExecuteInternal(CLIExecutionContext& context) const override;
108 +};
109 +
110 +// List Command
111 +struct ContainerListCommand final : public Command
112 +{
113 + constexpr static std::wstring_view CommandName = L"list";
114 + ContainerListCommand(const std::wstring& parent) : Command(CommandName, {L"ls", L"ps"}, parent)
115 + {
116 + }
117 + std::vector<Argument> GetArguments() const override;
118 + std::wstring ShortDescription() const override;
119 + std::wstring LongDescription() const override;
120 +
121 +protected:
122 + void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
123 + void ExecuteInternal(CLIExecutionContext& context) const override;
124 +};
125 +
126 +// Logs Command
127 +struct ContainerLogsCommand final : public Command
128 +{
129 + constexpr static std::wstring_view CommandName = L"logs";
130 + ContainerLogsCommand(const std::wstring& parent) : Command(CommandName, parent)
131 + {
132 + }
133 + std::vector<Argument> GetArguments() const override;
134 + std::wstring ShortDescription() const override;
135 + std::wstring LongDescription() const override;
136 +
137 +protected:
138 + void ExecuteInternal(CLIExecutionContext& context) const override;
139 +};
140 +
141 +// Remove Command
142 +struct ContainerRemoveCommand final : public Command
143 +{
144 + constexpr static std::wstring_view CommandName = L"remove";
145 + ContainerRemoveCommand(const std::wstring& parent) : Command(CommandName, {L"delete", L"rm"}, parent)
146 + {
147 + }
148 + std::vector<Argument> GetArguments() const override;
149 + std::wstring ShortDescription() const override;
150 + std::wstring LongDescription() const override;
151 +
152 +protected:
153 + void ExecuteInternal(CLIExecutionContext& context) const override;
154 +};
155 +
156 +// Run Command
157 +struct ContainerRunCommand final : public Command
158 +{
159 + constexpr static std::wstring_view CommandName = L"run";
160 + ContainerRunCommand(const std::wstring& parent) : Command(CommandName, parent)
161 + {
162 + }
163 + std::vector<Argument> GetArguments() const override;
164 + std::wstring ShortDescription() const override;
165 + std::wstring LongDescription() const override;
166 +
167 +protected:
168 + void ExecuteInternal(CLIExecutionContext& context) const override;
169 +};
170 +
171 +// Start Command
172 +struct ContainerStartCommand final : public Command
173 +{
174 + constexpr static std::wstring_view CommandName = L"start";
175 + ContainerStartCommand(const std::wstring& parent) : Command(CommandName, parent)
176 + {
177 + }
178 + std::vector<Argument> GetArguments() const override;
179 + std::wstring ShortDescription() const override;
180 + std::wstring LongDescription() const override;
181 +
182 +protected:
183 + void ExecuteInternal(CLIExecutionContext& context) const override;
184 +};
185 +
186 +// Stop Command
187 +struct ContainerStopCommand final : public Command
188 +{
189 + constexpr static std::wstring_view CommandName = L"stop";
190 + ContainerStopCommand(const std::wstring& parent) : Command(CommandName, parent)
191 + {
192 + }
193 + std::vector<Argument> GetArguments() const override;
194 + std::wstring ShortDescription() const override;
195 + std::wstring LongDescription() const override;
196 +
197 +protected:
198 + void ExecuteInternal(CLIExecutionContext& context) const override;
199 +};
200 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ContainerCreateCommand.cpp new
+84
@@ -0,0 +1,84 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerCreateCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Create Command
27 +std::vector<Argument> ContainerCreateCommand::GetArguments() const
28 +{
29 + // clang-format off
30 + return {
31 + Argument::Create(ArgType::ImageId, true),
32 + Argument::Create(ArgType::Command),
33 + Argument::Create(ArgType::ForwardArgs),
34 + // Argument::Create(ArgType::CIDFile),
35 + Argument::Create(ArgType::DNS, false, NO_LIMIT),
36 + // Argument::Create(ArgType::DNSDomain),
37 + Argument::Create(ArgType::DNSOption, false, NO_LIMIT),
38 + Argument::Create(ArgType::DNSSearch, false, NO_LIMIT),
39 + Argument::Create(ArgType::Domainname),
40 + Argument::Create(ArgType::Entrypoint),
41 + Argument::Create(ArgType::Env, false, NO_LIMIT),
42 + Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
43 + // Argument::Create(ArgType::GroupId),
44 + Argument::Create(ArgType::Hostname),
45 + Argument::Create(ArgType::Interactive),
46 + Argument::Create(ArgType::Label, false, NO_LIMIT),
47 + Argument::Create(ArgType::Name),
48 + // Argument::Create(ArgType::NoDNS),
49 + // Argument::Create(ArgType::Progress),
50 + Argument::Create(ArgType::Publish, false, NO_LIMIT),
51 + Argument::Create(ArgType::PublishAll),
52 + Argument::Create(ArgType::Remove),
53 + // Argument::Create(ArgType::Scheme),
54 + Argument::Create(ArgType::Session),
55 + Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
56 + Argument::Create(ArgType::TTY),
57 + Argument::Create(ArgType::User),
58 + Argument::Create(ArgType::Volume, false, NO_LIMIT),
59 + // Argument::Create(ArgType::Virtual),
60 + Argument::Create(ArgType::WorkDir),
61 + };
62 + // clang-format on
63 +}
64 +
65 +std::wstring ContainerCreateCommand::ShortDescription() const
66 +{
67 + return Localization::WSLCCLI_ContainerCreateDesc();
68 +}
69 +
70 +std::wstring ContainerCreateCommand::LongDescription() const
71 +{
72 + return Localization::WSLCCLI_ContainerCreateLongDesc();
73 +}
74 +
75 +// clang-format off
76 +void ContainerCreateCommand::ExecuteInternal(CLIExecutionContext& context) const
77 +{
78 + context
79 + << CreateSession
80 + << SetContainerOptionsFromArgs
81 + << CreateContainer;
82 +}
83 +// clang-format on
84 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ContainerExecCommand.cpp new
+62
@@ -0,0 +1,62 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerExecCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Exec Command
27 +std::vector<Argument> ContainerExecCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, true),
31 + Argument::Create(ArgType::Command, true),
32 + Argument::Create(ArgType::ForwardArgs, std::nullopt, std::nullopt, Localization::WSLCCLI_ContainerExecForwardArgsDescription()),
33 + Argument::Create(ArgType::Detach),
34 + Argument::Create(ArgType::Env, false, NO_LIMIT),
35 + Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
36 + Argument::Create(ArgType::Interactive),
37 + Argument::Create(ArgType::Session),
38 + Argument::Create(ArgType::TTY),
39 + Argument::Create(ArgType::User),
40 + Argument::Create(ArgType::WorkDir),
41 + };
42 +}
43 +
44 +std::wstring ContainerExecCommand::ShortDescription() const
45 +{
46 + return Localization::WSLCCLI_ContainerExecDesc();
47 +}
48 +
49 +std::wstring ContainerExecCommand::LongDescription() const
50 +{
51 + return Localization::WSLCCLI_ContainerExecLongDesc();
52 +}
53 +// clang-format off
54 +void ContainerExecCommand::ExecuteInternal(CLIExecutionContext& context) const
55 +{
56 + context
57 + << CreateSession
58 + << SetContainerOptionsFromArgs
59 + << ExecContainer;
60 +}
61 +// clang-format on
62 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/ContainerInspectCommand.cpp new
+53
@@ -0,0 +1,53 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerInspectCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Inspect Command
27 +std::vector<Argument> ContainerInspectCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
31 + Argument::Create(ArgType::Session),
32 + };
33 +}
34 +
35 +std::wstring ContainerInspectCommand::ShortDescription() const
36 +{
37 + return Localization::WSLCCLI_ContainerInspectDesc();
38 +}
39 +
40 +std::wstring ContainerInspectCommand::LongDescription() const
41 +{
42 + return Localization::WSLCCLI_ContainerInspectLongDesc();
43 +}
44 +
45 +// clang-format off
46 +void ContainerInspectCommand::ExecuteInternal(CLIExecutionContext& context) const
47 +{
48 + context
49 + << CreateSession
50 + << InspectContainers;
51 +}
52 +// clang-format on
53 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ContainerKillCommand.cpp new
+54
@@ -0,0 +1,54 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerKillCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Kill Command
27 +std::vector<Argument> ContainerKillCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
31 + Argument::Create(ArgType::Session),
32 + Argument::Create(ArgType::Signal),
33 + };
34 +}
35 +
36 +std::wstring ContainerKillCommand::ShortDescription() const
37 +{
38 + return Localization::WSLCCLI_ContainerKillDesc();
39 +}
40 +
41 +std::wstring ContainerKillCommand::LongDescription() const
42 +{
43 + return Localization::WSLCCLI_ContainerKillLongDesc();
44 +}
45 +
46 +// clang-format off
47 +void ContainerKillCommand::ExecuteInternal(CLIExecutionContext& context) const
48 +{
49 + context
50 + << CreateSession
51 + << KillContainers;
52 +}
53 +// clang-format on
54 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ContainerListCommand.cpp new
+70
@@ -0,0 +1,70 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerListCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +using namespace wsl::shared::string;
25 +
26 +namespace wsl::windows::wslc {
27 +// Container List Command
28 +std::vector<Argument> ContainerListCommand::GetArguments() const
29 +{
30 + return {
31 + Argument::Create(ArgType::All),
32 + Argument::Create(ArgType::Format),
33 + Argument::Create(ArgType::NoTrunc),
34 + Argument::Create(ArgType::Quiet),
35 + Argument::Create(ArgType::Session),
36 + };
37 +}
38 +
39 +std::wstring ContainerListCommand::ShortDescription() const
40 +{
41 + return Localization::WSLCCLI_ContainerListDesc();
42 +}
43 +
44 +std::wstring ContainerListCommand::LongDescription() const
45 +{
46 + return Localization::WSLCCLI_ContainerListLongDesc();
47 +}
48 +
49 +void ContainerListCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
50 +{
51 + if (execArgs.Contains(ArgType::Format))
52 + {
53 + auto format = execArgs.Get<ArgType::Format>();
54 + if (!IsEqual(format, L"json") && !IsEqual(format, L"table"))
55 + {
56 + throw CommandException(Localization::WSLCCLI_InvalidFormatError());
57 + }
58 + }
59 +}
60 +
61 +// clang-format off
62 +void ContainerListCommand::ExecuteInternal(CLIExecutionContext& context) const
63 +{
64 + context
65 + << CreateSession
66 + << GetContainers
67 + << ListContainers;
68 +}
69 +// clang-format on
70 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ContainerLogsCommand.cpp new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerLogsCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Logs Command
27 +std::vector<Argument> ContainerLogsCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, true),
31 + Argument::Create(ArgType::Session),
32 + Argument::Create(ArgType::Follow),
33 + };
34 +}
35 +
36 +std::wstring ContainerLogsCommand::ShortDescription() const
37 +{
38 + return Localization::WSLCCLI_ContainerLogsDesc();
39 +}
40 +
41 +std::wstring ContainerLogsCommand::LongDescription() const
42 +{
43 + return Localization::WSLCCLI_ContainerLogsLongDesc();
44 +}
45 +
46 +void ContainerLogsCommand::ExecuteInternal(CLIExecutionContext& context) const
47 +{
48 + context //
49 + << CreateSession //
50 + << ViewContainerLogs;
51 +}
52 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ContainerRemoveCommand.cpp new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerRemoveCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Remove Command
27 +std::vector<Argument> ContainerRemoveCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
31 + Argument::Create(ArgType::Force),
32 + Argument::Create(ArgType::Session),
33 + };
34 +}
35 +
36 +std::wstring ContainerRemoveCommand::ShortDescription() const
37 +{
38 + return Localization::WSLCCLI_ContainerRemoveDesc();
39 +}
40 +
41 +std::wstring ContainerRemoveCommand::LongDescription() const
42 +{
43 + return Localization::WSLCCLI_ContainerRemoveLongDesc();
44 +}
45 +
46 +void ContainerRemoveCommand::ExecuteInternal(CLIExecutionContext& context) const
47 +{
48 + context //
49 + << CreateSession //
50 + << RemoveContainers;
51 +}
52 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ContainerRunCommand.cpp new
+85
@@ -0,0 +1,85 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerRunCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Run Command
27 +std::vector<Argument> ContainerRunCommand::GetArguments() const
28 +{
29 + // clang-format off
30 + return {
31 + Argument::Create(ArgType::ImageId, true),
32 + Argument::Create(ArgType::Command),
33 + Argument::Create(ArgType::ForwardArgs),
34 + // Argument::Create(ArgType::CIDFile),
35 + Argument::Create(ArgType::Detach),
36 + Argument::Create(ArgType::DNS, false, NO_LIMIT),
37 + // Argument::Create(ArgType::DNSDomain),
38 + Argument::Create(ArgType::DNSOption, false, NO_LIMIT),
39 + Argument::Create(ArgType::DNSSearch, false, NO_LIMIT),
40 + Argument::Create(ArgType::Domainname),
41 + Argument::Create(ArgType::Entrypoint),
42 + Argument::Create(ArgType::Env, false, NO_LIMIT),
43 + Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
44 + Argument::Create(ArgType::Hostname),
45 + Argument::Create(ArgType::Interactive),
46 + Argument::Create(ArgType::Label, false, NO_LIMIT),
47 + Argument::Create(ArgType::Name),
48 + // Argument::Create(ArgType::NoDNS),
49 + // Argument::Create(ArgType::Progress),
50 + Argument::Create(ArgType::Publish, false, NO_LIMIT),
51 + Argument::Create(ArgType::PublishAll),
52 + // Argument::Create(ArgType::Pull),
53 + Argument::Create(ArgType::Remove),
54 + // Argument::Create(ArgType::Scheme),
55 + Argument::Create(ArgType::Session),
56 + Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
57 + Argument::Create(ArgType::TTY),
58 + Argument::Create(ArgType::User),
59 + Argument::Create(ArgType::Volume, false, NO_LIMIT),
60 + // Argument::Create(ArgType::Virtual),
61 + Argument::Create(ArgType::WorkDir),
62 + };
63 + // clang-format on
64 +}
65 +
66 +std::wstring ContainerRunCommand::ShortDescription() const
67 +{
68 + return Localization::WSLCCLI_ContainerRunDesc();
69 +}
70 +
71 +std::wstring ContainerRunCommand::LongDescription() const
72 +{
73 + return Localization::WSLCCLI_ContainerRunLongDesc();
74 +}
75 +
76 +// clang-format off
77 +void ContainerRunCommand::ExecuteInternal(CLIExecutionContext& context) const
78 +{
79 + context
80 + << CreateSession
81 + << SetContainerOptionsFromArgs
82 + << RunContainer;
83 +}
84 +// clang-format on
85 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/ContainerStartCommand.cpp new
+51
@@ -0,0 +1,51 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerStartCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Start Command
27 +std::vector<Argument> ContainerStartCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, true),
31 + Argument::Create(ArgType::Attach),
32 + Argument::Create(ArgType::Interactive), // NYI
33 + Argument::Create(ArgType::Session), // NYI
34 + };
35 +}
36 +
37 +std::wstring ContainerStartCommand::ShortDescription() const
38 +{
39 + return Localization::WSLCCLI_ContainerStartDesc();
40 +}
41 +
42 +std::wstring ContainerStartCommand::LongDescription() const
43 +{
44 + return Localization::WSLCCLI_ContainerStartLongDesc();
45 +}
46 +
47 +void ContainerStartCommand::ExecuteInternal(CLIExecutionContext& context) const
48 +{
49 + context << CreateSession << StartContainer;
50 +}
51 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/ContainerStopCommand.cpp new
+55
@@ -0,0 +1,55 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerStopCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Container Stop Command
27 +std::vector<Argument> ContainerStopCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, std::nullopt, NO_LIMIT),
31 + Argument::Create(ArgType::Session),
32 + Argument::Create(ArgType::Signal, std::nullopt, std::nullopt, Localization::WSLCCLI_SignalArgDescription(L"SIGTERM")),
33 + Argument::Create(ArgType::Time),
34 + };
35 +}
36 +
37 +std::wstring ContainerStopCommand::ShortDescription() const
38 +{
39 + return Localization::WSLCCLI_ContainerStopDesc();
40 +}
41 +
42 +std::wstring ContainerStopCommand::LongDescription() const
43 +{
44 + return Localization::WSLCCLI_ContainerStopLongDesc();
45 +}
46 +
47 +// clang-format off
48 +void ContainerStopCommand::ExecuteInternal(CLIExecutionContext& context) const
49 +{
50 + context
51 + << CreateSession
52 + << StopContainers;
53 +}
54 +// clang-format on
55 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImageBuildCommand.cpp new
+58
@@ -0,0 +1,58 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageBuildCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Image Build Command
27 +std::vector<Argument> ImageBuildCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::Path, true),
31 + Argument::Create(ArgType::BuildArg, false, NO_LIMIT),
32 + Argument::Create(ArgType::BuildPull),
33 + Argument::Create(ArgType::BuildTarget),
34 + Argument::Create(ArgType::File),
35 + Argument::Create(ArgType::NoCache),
36 + Argument::Create(ArgType::Session),
37 + Argument::Create(ArgType::Tag, false, NO_LIMIT),
38 + Argument::Create(ArgType::Verbose),
39 + };
40 +}
41 +
42 +std::wstring ImageBuildCommand::ShortDescription() const
43 +{
44 + return Localization::WSLCCLI_ImageBuildDesc();
45 +}
46 +
47 +std::wstring ImageBuildCommand::LongDescription() const
48 +{
49 + return Localization::WSLCCLI_ImageBuildLongDesc();
50 +}
51 +
52 +void ImageBuildCommand::ExecuteInternal(CLIExecutionContext& context) const
53 +{
54 + context //
55 + << CreateSession //
56 + << BuildImage;
57 +}
58 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImageCommand.cpp new
+57
@@ -0,0 +1,57 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +#include "CLIExecutionContext.h"
15 +#include "ImageCommand.h"
16 +
17 +using namespace wsl::windows::wslc::execution;
18 +using namespace wsl::shared;
19 +
20 +namespace wsl::windows::wslc {
21 +// Image Root Command
22 +std::vector<std::unique_ptr<Command>> ImageCommand::GetCommands() const
23 +{
24 + std::vector<std::unique_ptr<Command>> commands;
25 + commands.push_back(std::make_unique<ImageBuildCommand>(FullName()));
26 + commands.push_back(std::make_unique<ImageRemoveCommand>(FullName()));
27 + commands.push_back(std::make_unique<ImageInspectCommand>(FullName()));
28 + commands.push_back(std::make_unique<ImageListCommand>(FullName()));
29 + commands.push_back(std::make_unique<ImageLoadCommand>(FullName()));
30 + commands.push_back(std::make_unique<ImagePruneCommand>(FullName()));
31 + commands.push_back(std::make_unique<ImagePullCommand>(FullName()));
32 + commands.push_back(std::make_unique<ImagePushCommand>(FullName()));
33 + commands.push_back(std::make_unique<ImageSaveCommand>(FullName()));
34 + commands.push_back(std::make_unique<ImageTagCommand>(FullName()));
35 + return commands;
36 +}
37 +
38 +std::vector<Argument> ImageCommand::GetArguments() const
39 +{
40 + return {};
41 +}
42 +
43 +std::wstring ImageCommand::ShortDescription() const
44 +{
45 + return Localization::WSLCCLI_ImageCommandDesc();
46 +}
47 +
48 +std::wstring ImageCommand::LongDescription() const
49 +{
50 + return Localization::WSLCCLI_ImageCommandLongDesc();
51 +}
52 +
53 +void ImageCommand::ExecuteInternal(CLIExecutionContext& context) const
54 +{
55 + OutputHelp();
56 +}
57 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImageCommand.h new
+208
@@ -0,0 +1,208 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageCommand.h
8 +
9 +Abstract:
10 +
11 + Declaration of command classes and interfaces.
12 +
13 +--*/
14 +#pragma once
15 +#include "Command.h"
16 +
17 +namespace wsl::windows::wslc {
18 +// Root Image Command
19 +struct ImageCommand final : public Command
20 +{
21 + constexpr static std::wstring_view CommandName = L"image";
22 + ImageCommand(const std::wstring& parent) : Command(CommandName, parent)
23 + {
24 + }
25 + std::vector<Argument> GetArguments() const override;
26 + std::wstring ShortDescription() const override;
27 + std::wstring LongDescription() const override;
28 +
29 + std::vector<std::unique_ptr<Command>> GetCommands() const override;
30 +
31 +protected:
32 + void ExecuteInternal(CLIExecutionContext& context) const override;
33 +};
34 +
35 +// Build Command
36 +struct ImageBuildCommand final : public Command
37 +{
38 + constexpr static std::wstring_view CommandName = L"build";
39 + ImageBuildCommand(const std::wstring& parent) : Command(CommandName, parent)
40 + {
41 + }
42 + std::vector<Argument> GetArguments() const override;
43 + std::wstring ShortDescription() const override;
44 + std::wstring LongDescription() const override;
45 +
46 +protected:
47 + void ExecuteInternal(CLIExecutionContext& context) const override;
48 +};
49 +
50 +// List Command
51 +struct ImageListCommand final : public Command
52 +{
53 + constexpr static std::wstring_view CommandName = L"list";
54 +
55 + // When parented directly to the root, ImageListCommand uses a different name
56 + // to avoid colliding with the container list command.
57 + constexpr static std::wstring_view RootCommandName = L"images";
58 +
59 + ImageListCommand(const std::wstring& parent) : Command(CommandName, {L"ls"}, parent)
60 + {
61 + }
62 +
63 + // Image list has an alias 'images' off the root, which will collide with the
64 + // container list command and its alias off the root. To avoid this, we will use
65 + // an override constructor that changes the name and alias of the command for when
66 + // it is parented directly to the root.
67 + // The bool parameter is used as a tag to select the root-specific name.
68 + ImageListCommand(const std::wstring& parent, bool /*rootScoped*/) : Command(RootCommandName, {}, parent)
69 + {
70 + }
71 + std::vector<Argument> GetArguments() const override;
72 + std::wstring ShortDescription() const override;
73 + std::wstring LongDescription() const override;
74 +
75 +protected:
76 + void ExecuteInternal(CLIExecutionContext& context) const override;
77 +};
78 +
79 +// Load Command
80 +struct ImageLoadCommand final : public Command
81 +{
82 + constexpr static std::wstring_view CommandName = L"load";
83 + ImageLoadCommand(const std::wstring& parent) : Command(CommandName, parent)
84 + {
85 + }
86 + std::vector<Argument> GetArguments() const override;
87 + std::wstring ShortDescription() const override;
88 + std::wstring LongDescription() const override;
89 +
90 +protected:
91 + void ExecuteInternal(CLIExecutionContext& context) const override;
92 +};
93 +
94 +// Remove Command
95 +struct ImageRemoveCommand final : public Command
96 +{
97 + constexpr static std::wstring_view CommandName = L"remove";
98 +
99 + // When parented directly to the root, ImageRemoveCommand uses a different name
100 + constexpr static std::wstring_view RootCommandName = L"rmi";
101 +
102 + ImageRemoveCommand(const std::wstring& parent) : Command(CommandName, {L"delete", L"rm"}, parent)
103 + {
104 + }
105 +
106 + // Image remove has an alias 'rmi' off the root
107 + // The bool parameter is used as a tag to select the root-specific name.
108 + ImageRemoveCommand(const std::wstring& parent, bool /*rootScoped*/) : Command(RootCommandName, {}, parent)
109 + {
110 + }
111 + std::vector<Argument> GetArguments() const override;
112 + std::wstring ShortDescription() const override;
113 + std::wstring LongDescription() const override;
114 +
115 +protected:
116 + void ExecuteInternal(CLIExecutionContext& context) const override;
117 +};
118 +
119 +// Inspect Command
120 +struct ImageInspectCommand final : public Command
121 +{
122 + constexpr static std::wstring_view CommandName = L"inspect";
123 + ImageInspectCommand(const std::wstring& parent) : Command(CommandName, parent)
124 + {
125 + }
126 + std::vector<Argument> GetArguments() const override;
127 + std::wstring ShortDescription() const override;
128 + std::wstring LongDescription() const override;
129 +
130 +protected:
131 + void ExecuteInternal(CLIExecutionContext& context) const override;
132 +};
133 +
134 +// Pull Command
135 +struct ImagePullCommand final : public Command
136 +{
137 + constexpr static std::wstring_view CommandName = L"pull";
138 + ImagePullCommand(const std::wstring& parent) : Command(CommandName, parent)
139 + {
140 + }
141 + std::vector<Argument> GetArguments() const override;
142 + std::wstring ShortDescription() const override;
143 + std::wstring LongDescription() const override;
144 +
145 +protected:
146 + void ExecuteInternal(CLIExecutionContext& context) const override;
147 +};
148 +
149 +// Push Command
150 +struct ImagePushCommand final : public Command
151 +{
152 + constexpr static std::wstring_view CommandName = L"push";
153 + ImagePushCommand(const std::wstring& parent) : Command(CommandName, parent)
154 + {
155 + }
156 + std::vector<Argument> GetArguments() const override;
157 + std::wstring ShortDescription() const override;
158 + std::wstring LongDescription() const override;
159 +
160 +protected:
161 + void ExecuteInternal(CLIExecutionContext& context) const override;
162 +};
163 +
164 +// Save Command
165 +struct ImageSaveCommand final : public Command
166 +{
167 + constexpr static std::wstring_view CommandName = L"save";
168 + ImageSaveCommand(const std::wstring& parent) : Command(CommandName, parent)
169 + {
170 + }
171 + std::vector<Argument> GetArguments() const override;
172 + std::wstring ShortDescription() const override;
173 + std::wstring LongDescription() const override;
174 +
175 +protected:
176 + void ExecuteInternal(CLIExecutionContext& context) const override;
177 +};
178 +
179 +// Tag Command
180 +struct ImageTagCommand final : public Command
181 +{
182 + constexpr static std::wstring_view CommandName = L"tag";
183 + ImageTagCommand(const std::wstring& parent) : Command(CommandName, parent)
184 + {
185 + }
186 + std::vector<Argument> GetArguments() const override;
187 + std::wstring ShortDescription() const override;
188 + std::wstring LongDescription() const override;
189 +
190 +protected:
191 + void ExecuteInternal(CLIExecutionContext& context) const override;
192 +};
193 +
194 +// Prune Command
195 +struct ImagePruneCommand final : public Command
196 +{
197 + constexpr static std::wstring_view CommandName = L"prune";
198 + ImagePruneCommand(const std::wstring& parent) : Command(CommandName, parent)
199 + {
200 + }
201 + std::vector<Argument> GetArguments() const override;
202 + std::wstring ShortDescription() const override;
203 + std::wstring LongDescription() const override;
204 +
205 +protected:
206 + void ExecuteInternal(CLIExecutionContext& context) const override;
207 +};
208 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImageInspectCommand.cpp new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageInspectCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +using namespace wsl::shared::string;
25 +
26 +namespace wsl::windows::wslc {
27 +// Image Inspect Command
28 +std::vector<Argument> ImageInspectCommand::GetArguments() const
29 +{
30 + return {
31 + Argument::Create(ArgType::ImageId, true, NO_LIMIT),
32 + Argument::Create(ArgType::Session),
33 + };
34 +}
35 +
36 +std::wstring ImageInspectCommand::ShortDescription() const
37 +{
38 + return Localization::WSLCCLI_ImageInspectDesc();
39 +}
40 +
41 +std::wstring ImageInspectCommand::LongDescription() const
42 +{
43 + return Localization::WSLCCLI_ImageInspectLongDesc();
44 +}
45 +
46 +void ImageInspectCommand::ExecuteInternal(CLIExecutionContext& context) const
47 +{
48 + context //
49 + << CreateSession //
50 + << InspectImages;
51 +}
52 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImageListCommand.cpp new
+55
@@ -0,0 +1,55 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageListCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +using namespace wsl::shared::string;
25 +
26 +namespace wsl::windows::wslc {
27 +// Image List Command
28 +std::vector<Argument> ImageListCommand::GetArguments() const
29 +{
30 + return {
31 + Argument::Create(ArgType::Format),
32 + Argument::Create(ArgType::NoTrunc),
33 + Argument::Create(ArgType::Quiet),
34 + Argument::Create(ArgType::Session),
35 + Argument::Create(ArgType::Verbose)};
36 +}
37 +
38 +std::wstring ImageListCommand::ShortDescription() const
39 +{
40 + return Localization::WSLCCLI_ImageListDesc();
41 +}
42 +
43 +std::wstring ImageListCommand::LongDescription() const
44 +{
45 + return Localization::WSLCCLI_ImageListLongDesc();
46 +}
47 +
48 +void ImageListCommand::ExecuteInternal(CLIExecutionContext& context) const
49 +{
50 + context //
51 + << CreateSession //
52 + << GetImages //
53 + << ListImages;
54 +}
55 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImageLoadCommand.cpp new
+51
@@ -0,0 +1,51 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageLoadCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Image Load Command
27 +std::vector<Argument> ImageLoadCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::Input),
31 + Argument::Create(ArgType::Session),
32 + };
33 +}
34 +
35 +std::wstring ImageLoadCommand::ShortDescription() const
36 +{
37 + return Localization::WSLCCLI_ImageLoadDesc();
38 +}
39 +
40 +std::wstring ImageLoadCommand::LongDescription() const
41 +{
42 + return Localization::WSLCCLI_ImageLoadLongDesc();
43 +}
44 +
45 +void ImageLoadCommand::ExecuteInternal(CLIExecutionContext& context) const
46 +{
47 + context //
48 + << CreateSession //
49 + << LoadImage;
50 +}
51 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImagePruneCommand.cpp new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImagePruneCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +using namespace wsl::shared::string;
25 +
26 +namespace wsl::windows::wslc {
27 +// Image Prune Command
28 +std::vector<Argument> ImagePruneCommand::GetArguments() const
29 +{
30 + return {
31 + Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_ImagePruneAllArgDescription()),
32 + Argument::Create(ArgType::Session),
33 + };
34 +}
35 +
36 +std::wstring ImagePruneCommand::ShortDescription() const
37 +{
38 + return Localization::WSLCCLI_ImagePruneDesc();
39 +}
40 +
41 +std::wstring ImagePruneCommand::LongDescription() const
42 +{
43 + return Localization::WSLCCLI_ImagePruneLongDesc();
44 +}
45 +
46 +void ImagePruneCommand::ExecuteInternal(CLIExecutionContext& context) const
47 +{
48 + context //
49 + << CreateSession //
50 + << PruneImages;
51 +}
52 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/ImagePullCommand.cpp new
+53
@@ -0,0 +1,53 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImagePullCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Image Pull Command
27 +std::vector<Argument> ImagePullCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ImageId, true),
31 + // Argument::Create(ArgType::Scheme),
32 + // Argument::Create(ArgType::Progress),
33 + Argument::Create(ArgType::Session),
34 + };
35 +}
36 +
37 +std::wstring ImagePullCommand::ShortDescription() const
38 +{
39 + return Localization::WSLCCLI_ImagePullDesc();
40 +}
41 +
42 +std::wstring ImagePullCommand::LongDescription() const
43 +{
44 + return Localization::WSLCCLI_ImagePullLongDesc();
45 +}
46 +
47 +void ImagePullCommand::ExecuteInternal(CLIExecutionContext& context) const
48 +{
49 + context //
50 + << CreateSession //
51 + << PullImage;
52 +}
53 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImagePushCommand.cpp new
+51
@@ -0,0 +1,51 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImagePushCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the image push command.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Image Push Command
27 +std::vector<Argument> ImagePushCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ImageId, true),
31 + Argument::Create(ArgType::Session),
32 + };
33 +}
34 +
35 +std::wstring ImagePushCommand::ShortDescription() const
36 +{
37 + return Localization::WSLCCLI_ImagePushDesc();
38 +}
39 +
40 +std::wstring ImagePushCommand::LongDescription() const
41 +{
42 + return Localization::WSLCCLI_ImagePushLongDesc();
43 +}
44 +
45 +void ImagePushCommand::ExecuteInternal(CLIExecutionContext& context) const
46 +{
47 + context //
48 + << CreateSession //
49 + << PushImage;
50 +}
51 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/ImageRemoveCommand.cpp new
+54
@@ -0,0 +1,54 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageRemoveCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +using namespace wsl::shared::string;
25 +
26 +namespace wsl::windows::wslc {
27 +// Image Remove Command
28 +std::vector<Argument> ImageRemoveCommand::GetArguments() const
29 +{
30 + return {
31 + Argument::Create(ArgType::ImageId, true),
32 + Argument::Create(ArgType::ImageForce),
33 + Argument::Create(ArgType::NoPrune),
34 + Argument::Create(ArgType::Session),
35 + };
36 +}
37 +
38 +std::wstring ImageRemoveCommand::ShortDescription() const
39 +{
40 + return Localization::WSLCCLI_ImageRemoveDesc();
41 +}
42 +
43 +std::wstring ImageRemoveCommand::LongDescription() const
44 +{
45 + return Localization::WSLCCLI_ImageRemoveLongDesc();
46 +}
47 +
48 +void ImageRemoveCommand::ExecuteInternal(CLIExecutionContext& context) const
49 +{
50 + context //
51 + << CreateSession //
52 + << DeleteImage;
53 +}
54 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImageSaveCommand.cpp new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageSaveCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Image Save Command
27 +std::vector<Argument> ImageSaveCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ImageId, true),
31 + Argument::Create(ArgType::Output),
32 + Argument::Create(ArgType::Session),
33 + };
34 +}
35 +
36 +std::wstring ImageSaveCommand::ShortDescription() const
37 +{
38 + return Localization::WSLCCLI_ImageSaveDesc();
39 +}
40 +
41 +std::wstring ImageSaveCommand::LongDescription() const
42 +{
43 + return Localization::WSLCCLI_ImageSaveLongDesc();
44 +}
45 +
46 +void ImageSaveCommand::ExecuteInternal(CLIExecutionContext& context) const
47 +{
48 + context //
49 + << CreateSession //
50 + << SaveImage;
51 +}
52 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/ImageTagCommand.cpp new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageTagCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::shared;
22 +using namespace wsl::windows::wslc::execution;
23 +using namespace wsl::windows::wslc::task;
24 +
25 +namespace wsl::windows::wslc {
26 +// Image Tag Command
27 +std::vector<Argument> ImageTagCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::Source, true),
31 + Argument::Create(ArgType::Target, true),
32 + Argument::Create(ArgType::Session),
33 + };
34 +}
35 +
36 +std::wstring ImageTagCommand::ShortDescription() const
37 +{
38 + return Localization::WSLCCLI_ImageTagDesc();
39 +}
40 +
41 +std::wstring ImageTagCommand::LongDescription() const
42 +{
43 + return Localization::WSLCCLI_ImageTagLongDesc();
44 +}
45 +
46 +void ImageTagCommand::ExecuteInternal(CLIExecutionContext& context) const
47 +{
48 + context //
49 + << CreateSession //
50 + << TagImage;
51 +}
52 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/InspectCommand.cpp new
+46
@@ -0,0 +1,46 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + InspectCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the inspect command.
12 +--*/
13 +#include "InspectCommand.h"
14 +#include "SessionTasks.h"
15 +#include "InspectTasks.h"
16 +
17 +using namespace wsl::shared;
18 +using namespace wsl::windows::wslc::task;
19 +
20 +namespace wsl::windows::wslc {
21 +
22 +std::vector<Argument> InspectCommand::GetArguments() const
23 +{
24 + return {
25 + Argument::Create(ArgType::ObjectId, true, NO_LIMIT),
26 + Argument::Create(ArgType::Type),
27 + Argument::Create(ArgType::Session),
28 + };
29 +}
30 +
31 +std::wstring InspectCommand::ShortDescription() const
32 +{
33 + return {Localization::WSLCCLI_InspectDesc()};
34 +}
35 +
36 +std::wstring InspectCommand::LongDescription() const
37 +{
38 + return {Localization::WSLCCLI_InspectLongDesc()};
39 +}
40 +
41 +void InspectCommand::ExecuteInternal(CLIExecutionContext& context) const
42 +{
43 + context << CreateSession //
44 + << Inspect;
45 +}
46 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/InspectCommand.h new
+30
@@ -0,0 +1,30 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + InspectCommand.h
8 +
9 +Abstract:
10 +
11 + Declaration of the InspectCommand.
12 +--*/
13 +#pragma once
14 +#include "Command.h"
15 +
16 +namespace wsl::windows::wslc {
17 +struct InspectCommand final : public Command
18 +{
19 + constexpr static std::wstring_view CommandName = L"inspect";
20 + InspectCommand(const std::wstring& parent) : Command(CommandName, parent)
21 + {
22 + }
23 + std::vector<Argument> GetArguments() const override;
24 + std::wstring ShortDescription() const override;
25 + std::wstring LongDescription() const override;
26 +
27 +protected:
28 + void ExecuteInternal(CLIExecutionContext& context) const override;
29 +};
30 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/RegistryCommand.cpp new
+182
@@ -0,0 +1,182 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RegistryCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the registry command tree (login, logout).
12 +
13 +--*/
14 +
15 +#include "CLIExecutionContext.h"
16 +#include "RegistryCommand.h"
17 +#include "RegistryTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +#include <iostream>
21 +
22 +using namespace wsl::windows::wslc::execution;
23 +using namespace wsl::windows::wslc::task;
24 +using namespace wsl::shared;
25 +
26 +namespace {
27 +
28 +auto MaskInput()
29 +{
30 + HANDLE input = GetStdHandle(STD_INPUT_HANDLE);
31 + DWORD mode = 0;
32 +
33 + if ((input != INVALID_HANDLE_VALUE) && GetConsoleMode(input, &mode))
34 + {
35 + THROW_IF_WIN32_BOOL_FALSE(SetConsoleMode(input, mode & ~ENABLE_ECHO_INPUT));
36 + return wil::scope_exit(std::function<void()>([input, mode] {
37 + SetConsoleMode(input, mode);
38 + std::wcerr << L'\n';
39 + }));
40 + }
41 +
42 + return wil::scope_exit(std::function<void()>([] {}));
43 +}
44 +
45 +std::wstring Prompt(const std::wstring& label, bool maskInput)
46 +{
47 + // Write without a trailing newline so the cursor stays inline (matching Docker's behavior).
48 + std::wcerr << label;
49 +
50 + auto restoreConsole = maskInput ? MaskInput() : wil::scope_exit(std::function<void()>([] {}));
51 +
52 + std::wstring value;
53 + std::getline(std::wcin, value);
54 +
55 + return value;
56 +}
57 +
58 +} // namespace
59 +
60 +namespace wsl::windows::wslc {
61 +
62 +// Registry Root Command
63 +std::vector<std::unique_ptr<Command>> RegistryCommand::GetCommands() const
64 +{
65 + std::vector<std::unique_ptr<Command>> commands;
66 + commands.push_back(std::make_unique<RegistryLoginCommand>(FullName()));
67 + commands.push_back(std::make_unique<RegistryLogoutCommand>(FullName()));
68 + return commands;
69 +}
70 +
71 +std::vector<Argument> RegistryCommand::GetArguments() const
72 +{
73 + return {};
74 +}
75 +
76 +std::wstring RegistryCommand::ShortDescription() const
77 +{
78 + return Localization::WSLCCLI_RegistryCommandDesc();
79 +}
80 +
81 +std::wstring RegistryCommand::LongDescription() const
82 +{
83 + return Localization::WSLCCLI_RegistryCommandLongDesc();
84 +}
85 +
86 +void RegistryCommand::ExecuteInternal(CLIExecutionContext& context) const
87 +{
88 + OutputHelp();
89 +}
90 +
91 +// Registry Login Command
92 +std::vector<Argument> RegistryLoginCommand::GetArguments() const
93 +{
94 + return {
95 + Argument::Create(ArgType::Password),
96 + Argument::Create(ArgType::PasswordStdin),
97 + Argument::Create(ArgType::Username),
98 + Argument::Create(ArgType::Server),
99 + Argument::Create(ArgType::Session),
100 + };
101 +}
102 +
103 +std::wstring RegistryLoginCommand::ShortDescription() const
104 +{
105 + return Localization::WSLCCLI_LoginDesc();
106 +}
107 +
108 +std::wstring RegistryLoginCommand::LongDescription() const
109 +{
110 + return Localization::WSLCCLI_LoginLongDesc();
111 +}
112 +
113 +void RegistryLoginCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
114 +{
115 + if (execArgs.Contains(ArgType::Password) && execArgs.Contains(ArgType::PasswordStdin))
116 + {
117 + throw CommandException(Localization::WSLCCLI_LoginPasswordAndStdinMutuallyExclusive());
118 + }
119 +
120 + if (execArgs.Contains(ArgType::PasswordStdin) && !execArgs.Contains(ArgType::Username))
121 + {
122 + throw CommandException(Localization::WSLCCLI_LoginPasswordStdinRequiresUsername());
123 + }
124 +}
125 +
126 +void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const
127 +{
128 + // Prompt for username if not provided.
129 + if (!context.Args.Contains(ArgType::Username))
130 + {
131 + context.Args.Add(ArgType::Username, Prompt(Localization::WSLCCLI_LoginUsernamePrompt(), false));
132 + }
133 +
134 + // Resolve password: --password, --password-stdin, or interactive prompt.
135 + if (!context.Args.Contains(ArgType::Password))
136 + {
137 + if (context.Args.Contains(ArgType::PasswordStdin))
138 + {
139 + std::wstring line;
140 + std::getline(std::wcin, line);
141 + if (!line.empty() && line.back() == L'\r')
142 + {
143 + line.pop_back();
144 + }
145 +
146 + context.Args.Add(ArgType::Password, std::move(line));
147 + }
148 + else
149 + {
150 + context.Args.Add(ArgType::Password, Prompt(Localization::WSLCCLI_LoginPasswordPrompt(), true));
151 + }
152 + }
153 +
154 + context //
155 + << CreateSession << Login;
156 +}
157 +
158 +// Registry Logout Command
159 +std::vector<Argument> RegistryLogoutCommand::GetArguments() const
160 +{
161 + return {
162 + Argument::Create(ArgType::Server),
163 + };
164 +}
165 +
166 +std::wstring RegistryLogoutCommand::ShortDescription() const
167 +{
168 + return Localization::WSLCCLI_LogoutDesc();
169 +}
170 +
171 +std::wstring RegistryLogoutCommand::LongDescription() const
172 +{
173 + return Localization::WSLCCLI_LogoutLongDesc();
174 +}
175 +
176 +void RegistryLogoutCommand::ExecuteInternal(CLIExecutionContext& context) const
177 +{
178 + context //
179 + << Logout;
180 +}
181 +
182 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/RegistryCommand.h new
+71
@@ -0,0 +1,71 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RegistryCommand.h
8 +
9 +Abstract:
10 +
11 + Declaration of the registry command tree (login, logout).
12 +
13 +--*/
14 +#pragma once
15 +#include "Command.h"
16 +
17 +namespace wsl::windows::wslc {
18 +
19 +// Root registry command: wslc registry [login|logout]
20 +struct RegistryCommand final : public Command
21 +{
22 + constexpr static std::wstring_view CommandName = L"registry";
23 + RegistryCommand(const std::wstring& parent) : Command(CommandName, parent)
24 + {
25 + }
26 +
27 + std::vector<std::unique_ptr<Command>> GetCommands() const override;
28 + std::vector<Argument> GetArguments() const override;
29 + std::wstring ShortDescription() const override;
30 + std::wstring LongDescription() const override;
31 +
32 +protected:
33 + void ExecuteInternal(CLIExecutionContext& context) const override;
34 +};
35 +
36 +// Login Command
37 +struct RegistryLoginCommand final : public Command
38 +{
39 + constexpr static std::wstring_view CommandName = L"login";
40 +
41 + RegistryLoginCommand(const std::wstring& parent) : Command(CommandName, parent)
42 + {
43 + }
44 +
45 + std::vector<Argument> GetArguments() const override;
46 + std::wstring ShortDescription() const override;
47 + std::wstring LongDescription() const override;
48 +
49 +protected:
50 + void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
51 + void ExecuteInternal(CLIExecutionContext& context) const override;
52 +};
53 +
54 +// Logout Command
55 +struct RegistryLogoutCommand final : public Command
56 +{
57 + constexpr static std::wstring_view CommandName = L"logout";
58 +
59 + RegistryLogoutCommand(const std::wstring& parent) : Command(CommandName, parent)
60 + {
61 + }
62 +
63 + std::vector<Argument> GetArguments() const override;
64 + std::wstring ShortDescription() const override;
65 + std::wstring LongDescription() const override;
66 +
67 +protected:
68 + void ExecuteInternal(CLIExecutionContext& context) const override;
69 +};
70 +
71 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/RootCommand.cpp new
+91
@@ -0,0 +1,91 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RootCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the RootCommand, which is the root of all commands in the CLI.
12 +
13 +--*/
14 +#include "RootCommand.h"
15 +
16 +// Include all commands that parent to the root.
17 +#include "ContainerCommand.h"
18 +#include "ImageCommand.h"
19 +#include "RegistryCommand.h"
20 +#include "SessionCommand.h"
21 +#include "SettingsCommand.h"
22 +#include "InspectCommand.h"
23 +#include "VersionCommand.h"
24 +#include "VolumeCommand.h"
25 +
26 +using namespace wsl::windows::wslc::execution;
27 +using namespace wsl::shared;
28 +
29 +namespace wsl::windows::wslc {
30 +std::vector<std::unique_ptr<Command>> RootCommand::GetCommands() const
31 +{
32 + std::vector<std::unique_ptr<Command>> commands;
33 + commands.push_back(std::make_unique<ContainerCommand>(FullName()));
34 + commands.push_back(std::make_unique<ImageCommand>(FullName()));
35 + commands.push_back(std::make_unique<RegistryCommand>(FullName()));
36 + commands.push_back(std::make_unique<SessionCommand>(FullName()));
37 + commands.push_back(std::make_unique<SettingsCommand>(FullName()));
38 + commands.push_back(std::make_unique<VolumeCommand>(FullName()));
39 + commands.push_back(std::make_unique<ContainerAttachCommand>(FullName()));
40 + commands.push_back(std::make_unique<ImageBuildCommand>(FullName()));
41 + commands.push_back(std::make_unique<ContainerCreateCommand>(FullName()));
42 + commands.push_back(std::make_unique<ContainerExecCommand>(FullName()));
43 + commands.push_back(std::make_unique<ImageListCommand>(FullName(), true));
44 + commands.push_back(std::make_unique<InspectCommand>(FullName()));
45 + commands.push_back(std::make_unique<ContainerKillCommand>(FullName()));
46 + commands.push_back(std::make_unique<ContainerListCommand>(FullName()));
47 + commands.push_back(std::make_unique<ImageLoadCommand>(FullName()));
48 + commands.push_back(std::make_unique<RegistryLoginCommand>(FullName()));
49 + commands.push_back(std::make_unique<RegistryLogoutCommand>(FullName()));
50 + commands.push_back(std::make_unique<ContainerLogsCommand>(FullName()));
51 + commands.push_back(std::make_unique<ImagePullCommand>(FullName()));
52 + commands.push_back(std::make_unique<ImagePushCommand>(FullName()));
53 + commands.push_back(std::make_unique<ContainerRemoveCommand>(FullName()));
54 + commands.push_back(std::make_unique<ImageRemoveCommand>(FullName(), true));
55 + commands.push_back(std::make_unique<ContainerRunCommand>(FullName()));
56 + commands.push_back(std::make_unique<ImageSaveCommand>(FullName()));
57 + commands.push_back(std::make_unique<ContainerStartCommand>(FullName()));
58 + commands.push_back(std::make_unique<ContainerStopCommand>(FullName()));
59 + commands.push_back(std::make_unique<ImageTagCommand>(FullName()));
60 + commands.push_back(std::make_unique<VersionCommand>(FullName()));
61 + return commands;
62 +}
63 +
64 +std::vector<Argument> RootCommand::GetArguments() const
65 +{
66 + return {
67 + Argument::Create(ArgType::Version),
68 + };
69 +}
70 +
71 +std::wstring RootCommand::ShortDescription() const
72 +{
73 + return Localization::WSLCCLI_RootCommandDesc();
74 +}
75 +
76 +std::wstring RootCommand::LongDescription() const
77 +{
78 + return Localization::WSLCCLI_RootCommandLongDesc();
79 +}
80 +
81 +void RootCommand::ExecuteInternal(CLIExecutionContext& context) const
82 +{
83 + if (context.Args.Contains(ArgType::Version))
84 + {
85 + VersionCommand::PrintVersion();
86 + return;
87 + }
88 +
89 + OutputHelp();
90 +}
91 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/RootCommand.h new
+34
@@ -0,0 +1,34 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RootCommand.h
8 +
9 +Abstract:
10 +
11 + Declaration of the RootCommand, which is the root of all commands in the CLI.
12 +
13 +--*/
14 +#pragma once
15 +#include "Command.h"
16 +
17 +namespace wsl::windows::wslc {
18 +struct RootCommand final : public Command
19 +{
20 + constexpr static std::wstring_view CommandName = L"root";
21 +
22 + RootCommand() : Command(CommandName, {})
23 + {
24 + }
25 +
26 + std::vector<std::unique_ptr<Command>> GetCommands() const override;
27 + std::vector<Argument> GetArguments() const override;
28 + std::wstring ShortDescription() const override;
29 + std::wstring LongDescription() const override;
30 +
31 +protected:
32 + virtual void ExecuteInternal(CLIExecutionContext& context) const;
33 +};
34 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/SessionCommand.cpp new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of SessionCommand command tree.
12 +
13 +--*/
14 +#include "CLIExecutionContext.h"
15 +#include "ExecutionContextData.h"
16 +#include "SessionCommand.h"
17 +
18 +using namespace wsl::windows::wslc::execution;
19 +using namespace wsl::shared;
20 +
21 +namespace wsl::windows::wslc {
22 +// Session Root Command
23 +std::vector<std::unique_ptr<Command>> SessionCommand::GetCommands() const
24 +{
25 + std::vector<std::unique_ptr<Command>> commands;
26 + commands.push_back(std::make_unique<SessionEnterCommand>(FullName()));
27 + commands.push_back(std::make_unique<SessionListCommand>(FullName()));
28 + commands.push_back(std::make_unique<SessionShellCommand>(FullName()));
29 + commands.push_back(std::make_unique<SessionTerminateCommand>(FullName()));
30 + return commands;
31 +}
32 +
33 +std::vector<Argument> SessionCommand::GetArguments() const
34 +{
35 + return {};
36 +}
37 +
38 +std::wstring SessionCommand::ShortDescription() const
39 +{
40 + return Localization::WSLCCLI_SessionCommandDesc();
41 +}
42 +
43 +std::wstring SessionCommand::LongDescription() const
44 +{
45 + return Localization::WSLCCLI_SessionCommandLongDesc();
46 +}
47 +
48 +void SessionCommand::ExecuteInternal(CLIExecutionContext& context) const
49 +{
50 + OutputHelp();
51 +}
52 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/SessionCommand.h new
+94
@@ -0,0 +1,94 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionCommand.h
8 +
9 +Abstract:
10 +
11 + Declaration of SessionCommand command tree.
12 +
13 +--*/
14 +#pragma once
15 +#include "Command.h"
16 +
17 +namespace wsl::windows::wslc {
18 +// Root Session Command
19 +struct SessionCommand final : public Command
20 +{
21 + constexpr static std::wstring_view CommandName = L"session";
22 + SessionCommand(const std::wstring& parent) : Command(CommandName, parent)
23 + {
24 + }
25 + std::vector<Argument> GetArguments() const override;
26 + std::wstring ShortDescription() const override;
27 + std::wstring LongDescription() const override;
28 +
29 + std::vector<std::unique_ptr<Command>> GetCommands() const override;
30 +
31 +protected:
32 + void ExecuteInternal(CLIExecutionContext& context) const override;
33 +};
34 +
35 +// List Command
36 +struct SessionListCommand final : public Command
37 +{
38 + constexpr static std::wstring_view CommandName = L"list";
39 + SessionListCommand(const std::wstring& parent) : Command(CommandName, parent)
40 + {
41 + }
42 + std::vector<Argument> GetArguments() const override;
43 + std::wstring ShortDescription() const override;
44 + std::wstring LongDescription() const override;
45 +
46 +protected:
47 + void ExecuteInternal(CLIExecutionContext& context) const override;
48 +};
49 +
50 +// Shell Command
51 +struct SessionShellCommand final : public Command
52 +{
53 + constexpr static std::wstring_view CommandName = L"shell";
54 + SessionShellCommand(const std::wstring& parent) : Command(CommandName, parent)
55 + {
56 + }
57 + std::vector<Argument> GetArguments() const override;
58 + std::wstring ShortDescription() const override;
59 + std::wstring LongDescription() const override;
60 +
61 +protected:
62 + void ExecuteInternal(CLIExecutionContext& context) const override;
63 +};
64 +
65 +// Enter Command
66 +struct SessionEnterCommand final : public Command
67 +{
68 + constexpr static std::wstring_view CommandName = L"enter";
69 + SessionEnterCommand(const std::wstring& parent) : Command(CommandName, parent)
70 + {
71 + }
72 + std::vector<Argument> GetArguments() const override;
73 + std::wstring ShortDescription() const override;
74 + std::wstring LongDescription() const override;
75 +
76 +protected:
77 + void ExecuteInternal(CLIExecutionContext& context) const override;
78 +};
79 +
80 +// Terminate Command
81 +struct SessionTerminateCommand final : public Command
82 +{
83 + constexpr static std::wstring_view CommandName = L"terminate";
84 + SessionTerminateCommand(const std::wstring& parent) : Command(CommandName, parent)
85 + {
86 + }
87 + std::vector<Argument> GetArguments() const override;
88 + std::wstring ShortDescription() const override;
89 + std::wstring LongDescription() const override;
90 +
91 +protected:
92 + void ExecuteInternal(CLIExecutionContext& context) const override;
93 +};
94 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/SessionEnterCommand.cpp new
+48
@@ -0,0 +1,48 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionEnterCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the session enter command.
12 +
13 +--*/
14 +
15 +#include "CLIExecutionContext.h"
16 +#include "SessionCommand.h"
17 +#include "SessionTasks.h"
18 +#include "Task.h"
19 +
20 +using namespace wsl::windows::wslc::execution;
21 +using namespace wsl::windows::wslc::task;
22 +using namespace wsl::shared;
23 +
24 +namespace wsl::windows::wslc {
25 +
26 +std::vector<Argument> SessionEnterCommand::GetArguments() const
27 +{
28 + return {
29 + Argument::Create(ArgType::StoragePath, true),
30 + Argument::Create(ArgType::Name, std::nullopt, std::nullopt, Localization::WSLCCLI_SessionEnterNameArgDescription()),
31 + };
32 +}
33 +
34 +std::wstring SessionEnterCommand::ShortDescription() const
35 +{
36 + return Localization::WSLCCLI_SessionEnterDesc();
37 +}
38 +
39 +std::wstring SessionEnterCommand::LongDescription() const
40 +{
41 + return Localization::WSLCCLI_SessionEnterLongDesc();
42 +}
43 +
44 +void SessionEnterCommand::ExecuteInternal(CLIExecutionContext& context) const
45 +{
46 + context << EnterSession;
47 +}
48 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/SessionListCommand.cpp new
+46
@@ -0,0 +1,46 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionListCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the session list command.
12 +
13 +--*/
14 +#include "CLIExecutionContext.h"
15 +#include "SessionCommand.h"
16 +#include "SessionTasks.h"
17 +#include "Task.h"
18 +
19 +using namespace wsl::windows::wslc::execution;
20 +using namespace wsl::windows::wslc::task;
21 +using namespace wsl::shared;
22 +
23 +namespace wsl::windows::wslc {
24 +// Session List Command
25 +std::vector<Argument> SessionListCommand::GetArguments() const
26 +{
27 + return {
28 + Argument::Create(ArgType::Verbose, std::nullopt, std::nullopt, Localization::WSLCCLI_SessionListVerboseArgDescription()),
29 + };
30 +}
31 +
32 +std::wstring SessionListCommand::ShortDescription() const
33 +{
34 + return Localization::WSLCCLI_SessionListDesc();
35 +}
36 +
37 +std::wstring SessionListCommand::LongDescription() const
38 +{
39 + return Localization::WSLCCLI_SessionListLongDesc();
40 +}
41 +
42 +void SessionListCommand::ExecuteInternal(CLIExecutionContext& context) const
43 +{
44 + context << ListSessions;
45 +}
46 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/SessionShellCommand.cpp new
+46
@@ -0,0 +1,46 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionShellCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the session shell command.
12 +
13 +--*/
14 +#include "CLIExecutionContext.h"
15 +#include "SessionCommand.h"
16 +#include "SessionTasks.h"
17 +#include "Task.h"
18 +
19 +using namespace wsl::windows::wslc::execution;
20 +using namespace wsl::windows::wslc::task;
21 +using namespace wsl::shared;
22 +
23 +namespace wsl::windows::wslc {
24 +// Session Shell Command
25 +std::vector<Argument> SessionShellCommand::GetArguments() const
26 +{
27 + return {
28 + Argument::Create(ArgType::SessionId),
29 + };
30 +}
31 +
32 +std::wstring SessionShellCommand::ShortDescription() const
33 +{
34 + return Localization::WSLCCLI_SessionShellDesc();
35 +}
36 +
37 +std::wstring SessionShellCommand::LongDescription() const
38 +{
39 + return Localization::WSLCCLI_SessionShellLongDesc();
40 +}
41 +
42 +void SessionShellCommand::ExecuteInternal(CLIExecutionContext& context) const
43 +{
44 + context << AttachToSession;
45 +}
46 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/SessionTerminateCommand.cpp new
+46
@@ -0,0 +1,46 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionTerminateCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the session terminate command.
12 +
13 +--*/
14 +#include "CLIExecutionContext.h"
15 +#include "SessionCommand.h"
16 +#include "SessionTasks.h"
17 +#include "Task.h"
18 +
19 +using namespace wsl::windows::wslc::execution;
20 +using namespace wsl::windows::wslc::task;
21 +using namespace wsl::shared;
22 +
23 +namespace wsl::windows::wslc {
24 +// Session Terminate Command
25 +std::vector<Argument> SessionTerminateCommand::GetArguments() const
26 +{
27 + return {
28 + Argument::Create(ArgType::SessionId),
29 + };
30 +}
31 +
32 +std::wstring SessionTerminateCommand::ShortDescription() const
33 +{
34 + return Localization::WSLCCLI_SessionTerminateDesc();
35 +}
36 +
37 +std::wstring SessionTerminateCommand::LongDescription() const
38 +{
39 + return Localization::WSLCCLI_SessionTerminateLongDesc();
40 +}
41 +
42 +void SessionTerminateCommand::ExecuteInternal(CLIExecutionContext& context) const
43 +{
44 + context << TerminateSession;
45 +}
46 +} // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/SettingsCommand.cpp new
+89
@@ -0,0 +1,89 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SettingsCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of SettingsCommand command tree.
12 +
13 +--*/
14 +#include "Argument.h"
15 +#include "SettingsCommand.h"
16 +#include "WSLCUserSettings.h"
17 +#include "wslutil.h"
18 +
19 +using namespace wsl::windows::common::wslutil;
20 +using namespace wsl::windows::wslc::execution;
21 +using namespace wsl::shared;
22 +
23 +namespace wsl::windows::wslc {
24 +
25 +// SettingsCommand
26 +std::vector<std::unique_ptr<Command>> SettingsCommand::GetCommands() const
27 +{
28 + std::vector<std::unique_ptr<Command>> commands;
29 + commands.push_back(std::make_unique<SettingsResetCommand>(FullName()));
30 + return commands;
31 +}
32 +
33 +std::vector<Argument> SettingsCommand::GetArguments() const
34 +{
35 + return {};
36 +}
37 +
38 +std::wstring SettingsCommand::ShortDescription() const
39 +{
40 + return Localization::WSLCCLI_SettingsCommandDesc();
41 +}
42 +
43 +std::wstring SettingsCommand::LongDescription() const
44 +{
45 + return Localization::WSLCCLI_SettingsCommandLongDesc();
46 +}
47 +
48 +void SettingsCommand::ExecuteInternal(CLIExecutionContext& context) const
49 +{
50 + const auto& userSettings = settings::User();
51 + userSettings.PrepareToShellExecuteFile();
52 + const auto path = userSettings.SettingsFilePath();
53 +
54 + // Some versions of windows will fail if no file extension association exists, other will pop up the dialog
55 + // to make the user pick their default.
56 + HINSTANCE res = ShellExecuteW(nullptr, nullptr, path.c_str(), nullptr, nullptr, SW_SHOW);
57 + if (static_cast<int>(reinterpret_cast<uintptr_t>(res)) <= 32)
58 + {
59 + // User doesn't have file type association. Default to notepad
60 + // Quote the path so that Notepad treats it as a single argument even if it contains spaces.
61 + std::filesystem::path notepadPath = std::filesystem::path{wil::GetSystemDirectoryW().get()} / L"notepad.exe";
62 + std::wstring quotedPath = L"\"" + path.wstring() + L"\"";
63 + ShellExecuteW(nullptr, nullptr, notepadPath.c_str(), quotedPath.c_str(), nullptr, SW_SHOW);
64 + }
65 +}
66 +
67 +// SettingsResetCommand
68 +std::vector<Argument> SettingsResetCommand::GetArguments() const
69 +{
70 + return {};
71 +}
72 +
73 +std::wstring SettingsResetCommand::ShortDescription() const
74 +{
75 + return Localization::WSLCCLI_SettingsResetDesc();
76 +}
77 +
78 +std::wstring SettingsResetCommand::LongDescription() const
79 +{
80 + return Localization::WSLCCLI_SettingsResetLongDesc();
81 +}
82 +
83 +void SettingsResetCommand::ExecuteInternal(CLIExecutionContext& context) const
84 +{
85 + settings::User().Reset();
86 + PrintMessage(Localization::WSLCCLI_SettingsResetConfirm());
87 +}
88 +
89 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/SettingsCommand.h new
+54
@@ -0,0 +1,54 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SettingsCommand.h
8 +
9 +Abstract:
10 +
11 + Declaration of SettingsCommand command tree.
12 +
13 +--*/
14 +#pragma once
15 +#include "Command.h"
16 +
17 +namespace wsl::windows::wslc {
18 +
19 +// Root settings command: opens the settings file in the user's default editor.
20 +struct SettingsCommand final : public Command
21 +{
22 + constexpr static std::wstring_view CommandName = L"settings";
23 +
24 + SettingsCommand(const std::wstring& parent) : Command(CommandName, parent)
25 + {
26 + }
27 +
28 + std::vector<std::unique_ptr<Command>> GetCommands() const override;
29 + std::vector<Argument> GetArguments() const override;
30 + std::wstring ShortDescription() const override;
31 + std::wstring LongDescription() const override;
32 +
33 +protected:
34 + void ExecuteInternal(CLIExecutionContext& context) const override;
35 +};
36 +
37 +// Resets the settings file to built-in defaults.
38 +struct SettingsResetCommand final : public Command
39 +{
40 + constexpr static std::wstring_view CommandName = L"reset";
41 +
42 + SettingsResetCommand(const std::wstring& parent) : Command(CommandName, parent)
43 + {
44 + }
45 +
46 + std::vector<Argument> GetArguments() const override;
47 + std::wstring ShortDescription() const override;
48 + std::wstring LongDescription() const override;
49 +
50 +protected:
51 + void ExecuteInternal(CLIExecutionContext& context) const override;
52 +};
53 +
54 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/VersionCommand.cpp new
+40
@@ -0,0 +1,40 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VersionCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the version command.
12 +
13 +--*/
14 +#include "VersionCommand.h"
15 +
16 +using namespace wsl::shared;
17 +using namespace wsl::windows::wslc::execution;
18 +
19 +namespace wsl::windows::wslc {
20 +std::wstring VersionCommand::ShortDescription() const
21 +{
22 + return Localization::WSLCCLI_VersionDesc();
23 +}
24 +
25 +std::wstring VersionCommand::LongDescription() const
26 +{
27 + return Localization::WSLCCLI_VersionLongDesc();
28 +}
29 +
30 +void VersionCommand::PrintVersion()
31 +{
32 + wsl::windows::common::wslutil::PrintMessage(std::format(L"{} {}", s_ExecutableName, WSL_PACKAGE_VERSION));
33 +}
34 +
35 +void VersionCommand::ExecuteInternal(CLIExecutionContext& context) const
36 +{
37 + UNREFERENCED_PARAMETER(context);
38 + PrintVersion();
39 +}
40 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/VersionCommand.h new
+31
@@ -0,0 +1,31 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VersionCommand.h
8 +
9 +Abstract:
10 +
11 + Declaration of the VersionCommand.
12 +
13 +--*/
14 +#pragma once
15 +#include "Command.h"
16 +
17 +namespace wsl::windows::wslc {
18 +struct VersionCommand final : public Command
19 +{
20 + constexpr static std::wstring_view CommandName = L"version";
21 + VersionCommand(const std::wstring& parent) : Command(CommandName, parent)
22 + {
23 + }
24 + static void PrintVersion();
25 + std::wstring ShortDescription() const override;
26 + std::wstring LongDescription() const override;
27 +
28 +protected:
29 + void ExecuteInternal(CLIExecutionContext& context) const override;
30 +};
31 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/VolumeCommand.cpp new
+51
@@ -0,0 +1,51 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +#include "CLIExecutionContext.h"
15 +#include "VolumeCommand.h"
16 +
17 +using namespace wsl::windows::wslc::execution;
18 +using namespace wsl::shared;
19 +
20 +namespace wsl::windows::wslc {
21 +// Volume Root Command
22 +std::vector<std::unique_ptr<Command>> VolumeCommand::GetCommands() const
23 +{
24 + std::vector<std::unique_ptr<Command>> commands;
25 + commands.push_back(std::make_unique<VolumeCreateCommand>(FullName()));
26 + commands.push_back(std::make_unique<VolumeRemoveCommand>(FullName()));
27 + commands.push_back(std::make_unique<VolumeInspectCommand>(FullName()));
28 + commands.push_back(std::make_unique<VolumeListCommand>(FullName()));
29 + return commands;
30 +}
31 +
32 +std::vector<Argument> VolumeCommand::GetArguments() const
33 +{
34 + return {};
35 +}
36 +
37 +std::wstring VolumeCommand::ShortDescription() const
38 +{
39 + return Localization::WSLCCLI_VolumeCommandDesc();
40 +}
41 +
42 +std::wstring VolumeCommand::LongDescription() const
43 +{
44 + return Localization::WSLCCLI_VolumeCommandLongDesc();
45 +}
46 +
47 +void VolumeCommand::ExecuteInternal(CLIExecutionContext& context) const
48 +{
49 + OutputHelp();
50 +}
51 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/VolumeCommand.h new
+95
@@ -0,0 +1,95 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeCommand.h
8 +
9 +Abstract:
10 +
11 + Declaration of command classes and interfaces.
12 +
13 +--*/
14 +#pragma once
15 +#include "Command.h"
16 +
17 +namespace wsl::windows::wslc {
18 +// Root Volume Command
19 +struct VolumeCommand final : public Command
20 +{
21 + constexpr static std::wstring_view CommandName = L"volume";
22 + VolumeCommand(const std::wstring& parent) : Command(CommandName, parent)
23 + {
24 + }
25 + std::vector<Argument> GetArguments() const override;
26 + std::wstring ShortDescription() const override;
27 + std::wstring LongDescription() const override;
28 +
29 + std::vector<std::unique_ptr<Command>> GetCommands() const override;
30 +
31 +protected:
32 + void ExecuteInternal(CLIExecutionContext& context) const override;
33 +};
34 +
35 +// Create Command
36 +struct VolumeCreateCommand final : public Command
37 +{
38 + constexpr static std::wstring_view CommandName = L"create";
39 + VolumeCreateCommand(const std::wstring& parent) : Command(CommandName, parent)
40 + {
41 + }
42 + std::vector<Argument> GetArguments() const override;
43 + std::wstring ShortDescription() const override;
44 + std::wstring LongDescription() const override;
45 +
46 +protected:
47 + void ExecuteInternal(CLIExecutionContext& context) const override;
48 +};
49 +
50 +// Remove Command
51 +struct VolumeRemoveCommand final : public Command
52 +{
53 + constexpr static std::wstring_view CommandName = L"remove";
54 + VolumeRemoveCommand(const std::wstring& parent) : Command(CommandName, {L"delete", L"rm"}, parent)
55 + {
56 + }
57 + std::vector<Argument> GetArguments() const override;
58 + std::wstring ShortDescription() const override;
59 + std::wstring LongDescription() const override;
60 +
61 +protected:
62 + void ExecuteInternal(CLIExecutionContext& context) const override;
63 +};
64 +
65 +// Inspect Command
66 +struct VolumeInspectCommand final : public Command
67 +{
68 + constexpr static std::wstring_view CommandName = L"inspect";
69 + VolumeInspectCommand(const std::wstring& parent) : Command(CommandName, parent)
70 + {
71 + }
72 + std::vector<Argument> GetArguments() const override;
73 + std::wstring ShortDescription() const override;
74 + std::wstring LongDescription() const override;
75 +
76 +protected:
77 + void ExecuteInternal(CLIExecutionContext& context) const override;
78 +};
79 +
80 +// List Command
81 +struct VolumeListCommand final : public Command
82 +{
83 + constexpr static std::wstring_view CommandName = L"list";
84 + VolumeListCommand(const std::wstring& parent) : Command(CommandName, {L"ls"}, parent)
85 + {
86 + }
87 + std::vector<Argument> GetArguments() const override;
88 + std::wstring ShortDescription() const override;
89 + std::wstring LongDescription() const override;
90 +
91 +protected:
92 + void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
93 + void ExecuteInternal(CLIExecutionContext& context) const override;
94 +};
95 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/VolumeCreateCommand.cpp new
+53
@@ -0,0 +1,53 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeCreateCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "VolumeCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "SessionTasks.h"
18 +#include "VolumeTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Volume Create Command
27 +std::vector<Argument> VolumeCreateCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::VolumeName),
31 + Argument::Create(ArgType::Driver),
32 + Argument::Create(ArgType::Options, false, NO_LIMIT),
33 + Argument::Create(ArgType::Label, false, NO_LIMIT),
34 + Argument::Create(ArgType::Session),
35 + };
36 +}
37 +
38 +std::wstring VolumeCreateCommand::ShortDescription() const
39 +{
40 + return Localization::WSLCCLI_VolumeCreateDesc();
41 +}
42 +
43 +std::wstring VolumeCreateCommand::LongDescription() const
44 +{
45 + return Localization::WSLCCLI_VolumeCreateLongDesc();
46 +}
47 +
48 +void VolumeCreateCommand::ExecuteInternal(CLIExecutionContext& context) const
49 +{
50 + context << CreateSession //
51 + << CreateVolume;
52 +}
53 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/VolumeInspectCommand.cpp new
+50
@@ -0,0 +1,50 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeInspectCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "VolumeCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "SessionTasks.h"
18 +#include "VolumeTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Volume Inspect Command
27 +std::vector<Argument> VolumeInspectCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::VolumeName, true, NO_LIMIT),
31 + Argument::Create(ArgType::Session),
32 + };
33 +}
34 +
35 +std::wstring VolumeInspectCommand::ShortDescription() const
36 +{
37 + return Localization::WSLCCLI_VolumeInspectDesc();
38 +}
39 +
40 +std::wstring VolumeInspectCommand::LongDescription() const
41 +{
42 + return Localization::WSLCCLI_VolumeInspectLongDesc();
43 +}
44 +
45 +void VolumeInspectCommand::ExecuteInternal(CLIExecutionContext& context) const
46 +{
47 + context << CreateSession //
48 + << InspectVolumes;
49 +}
50 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/VolumeListCommand.cpp new
+65
@@ -0,0 +1,65 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeListCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "VolumeCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "SessionTasks.h"
18 +#include "VolumeTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +using namespace wsl::shared::string;
25 +
26 +namespace wsl::windows::wslc {
27 +// Volume List Command
28 +std::vector<Argument> VolumeListCommand::GetArguments() const
29 +{
30 + return {
31 + Argument::Create(ArgType::Format),
32 + Argument::Create(ArgType::Quiet, false, std::nullopt, Localization::WSLCCLI_VolumeListQuietArgDesc()),
33 + Argument::Create(ArgType::Session),
34 + };
35 +}
36 +
37 +std::wstring VolumeListCommand::ShortDescription() const
38 +{
39 + return Localization::WSLCCLI_VolumeListDesc();
40 +}
41 +
42 +std::wstring VolumeListCommand::LongDescription() const
43 +{
44 + return Localization::WSLCCLI_VolumeListLongDesc();
45 +}
46 +
47 +void VolumeListCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
48 +{
49 + if (execArgs.Contains(ArgType::Format))
50 + {
51 + auto format = execArgs.Get<ArgType::Format>();
52 + if (!IsEqual(format, L"json") && !IsEqual(format, L"table"))
53 + {
54 + throw CommandException(Localization::WSLCCLI_InvalidFormatError());
55 + }
56 + }
57 +}
58 +
59 +void VolumeListCommand::ExecuteInternal(CLIExecutionContext& context) const
60 +{
61 + context << CreateSession //
62 + << GetVolumes //
63 + << ListVolumes;
64 +}
65 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/VolumeRemoveCommand.cpp new
+50
@@ -0,0 +1,50 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeRemoveCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "VolumeCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "SessionTasks.h"
18 +#include "VolumeTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Volume Delete Command
27 +std::vector<Argument> VolumeRemoveCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::VolumeName, true, NO_LIMIT),
31 + Argument::Create(ArgType::Session),
32 + };
33 +}
34 +
35 +std::wstring VolumeRemoveCommand::ShortDescription() const
36 +{
37 + return Localization::WSLCCLI_VolumeRemoveDesc();
38 +}
39 +
40 +std::wstring VolumeRemoveCommand::LongDescription() const
41 +{
42 + return Localization::WSLCCLI_VolumeRemoveLongDesc();
43 +}
44 +
45 +void VolumeRemoveCommand::ExecuteInternal(CLIExecutionContext& context) const
46 +{
47 + context << CreateSession //
48 + << DeleteVolumes;
49 +}
50 +} // namespace wsl::windows::wslc
src/windows/wslc/core/CLIExecutionContext.h new
+54
@@ -0,0 +1,54 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + CLIExecutionContext.h
8 +
9 +Abstract:
10 +
11 + Declaration of CLI execution context.
12 +
13 +--*/
14 +#pragma once
15 +#include "ArgumentTypes.h"
16 +#include "ExecutionContextData.h"
17 +#include <optional>
18 +
19 +namespace wsl::windows::wslc::execution {
20 +// The context within which all commands execute.
21 +// Contains arguments via Args.
22 +struct CLIExecutionContext : public wsl::windows::common::ExecutionContext
23 +{
24 + CLIExecutionContext() : wsl::windows::common::ExecutionContext(wsl::windows::common::Context::WslC)
25 + {
26 + }
27 + ~CLIExecutionContext() override = default;
28 +
29 + NON_COPYABLE(CLIExecutionContext);
30 + CLIExecutionContext(CLIExecutionContext&&) = default;
31 + CLIExecutionContext& operator=(CLIExecutionContext&&) = default;
32 +
33 + argument::ArgMap Args;
34 +
35 + // Map of data stored in the context.
36 + DataMap Data;
37 +
38 + // Process exit code set by tasks like Run/Exec. When set, CoreMain returns this
39 + // instead of the HRESULT, enabling `wslc run ... && echo success` patterns.
40 + std::optional<int> ExitCode;
41 +
42 + // Event signaled when the user presses Ctrl-C. Starts null; long-running operations
43 + // that support cancellation create it via CreateCancelEvent() before passing it to
44 + // COM APIs that accept a CancelEvent handle.
45 + wil::unique_event CancelEvent;
46 +
47 + HANDLE CreateCancelEvent()
48 + {
49 + WI_ASSERT(!CancelEvent);
50 + CancelEvent.create(wil::EventOptions::ManualReset);
51 + return CancelEvent.get();
52 + }
53 +};
54 +} // namespace wsl::windows::wslc::execution
src/windows/wslc/core/Command.cpp new
+385
@@ -0,0 +1,385 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + Command.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +#include "Argument.h"
15 +#include "Command.h"
16 +#include "Invocation.h"
17 +#include "ArgumentParser.h"
18 +
19 +using namespace wsl::shared;
20 +using namespace wsl::windows::common::wslutil;
21 +using namespace wsl::windows::wslc::execution;
22 +
23 +namespace wsl::windows::wslc {
24 +
25 +Command::Command(std::wstring_view name, std::vector<std::wstring_view>&& aliases, const std::wstring& parent) :
26 + m_name(name), m_aliases(std::move(aliases))
27 +{
28 + if (!parent.empty())
29 + {
30 + m_fullName.reserve(parent.length() + 1 + name.length());
31 + m_fullName = parent;
32 + m_fullName += ParentSplitChar;
33 + m_fullName += name;
34 + }
35 + else
36 + {
37 + m_fullName = name;
38 + }
39 +}
40 +
41 +// This is the header applied before every help output.
42 +// It is separate in case we need to show it in other contexts, such as error messages, or
43 +// during specific command executions.
44 +void Command::OutputIntroHeader() const
45 +{
46 + std::wostringstream infoOut;
47 + infoOut << Localization::WSLCCLI_CopyrightHeader() << std::endl;
48 + PrintMessage(infoOut.str(), stdout);
49 +}
50 +
51 +void Command::OutputHelp(const CommandException* exception) const
52 +{
53 + // Header
54 + OutputIntroHeader();
55 +
56 + // Error if given
57 + if (exception)
58 + {
59 + PrintMessage(exception->Message(), stderr);
60 + }
61 +
62 + // Description
63 + std::wostringstream infoOut;
64 + infoOut << LongDescription() << std::endl << std::endl;
65 +
66 + // Example usage for this command
67 + // First create the command chain for output
68 + std::wstring commandChain = FullName();
69 + size_t firstSplit = commandChain.find_first_of(ParentSplitChar);
70 + if (firstSplit == std::wstring::npos)
71 + {
72 + commandChain.clear();
73 + }
74 + else
75 + {
76 + commandChain = commandChain.substr(firstSplit + 1);
77 + for (wchar_t& c : commandChain)
78 + {
79 + if (c == ParentSplitChar)
80 + {
81 + c = L' ';
82 + }
83 + }
84 + }
85 +
86 + // Usage follows the Microsoft convention:
87 + // https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/command-line-syntax-key
88 +
89 + // Output the command preamble and command chain
90 + infoOut << Localization::WSLCCLI_Usage(s_ExecutableName, std::wstring_view{commandChain});
91 +
92 + auto commandAliases = Aliases();
93 + auto commands = GetCommands();
94 + auto arguments = GetAllArguments();
95 +
96 + // Separate arguments by Kind
97 + std::vector<Argument> standardArgs;
98 + std::vector<Argument> positionalArgs;
99 + std::vector<Argument> forwardArgs;
100 + bool requiredPositionalArgsExist = false;
101 + for (const auto& arg : arguments)
102 + {
103 + switch (arg.Kind())
104 + {
105 + case Kind::Flag:
106 + standardArgs.emplace_back(arg);
107 + break;
108 + case Kind::Value:
109 + standardArgs.emplace_back(arg);
110 + break;
111 + case Kind::Positional:
112 + positionalArgs.emplace_back(arg);
113 + if (arg.Required())
114 + {
115 + requiredPositionalArgsExist = true;
116 + }
117 + break;
118 + case Kind::Forward:
119 + forwardArgs.emplace_back(arg);
120 + break;
121 + }
122 + }
123 +
124 + bool hasArguments = !positionalArgs.empty();
125 + bool hasOptions = !standardArgs.empty();
126 + bool hasForwardArgs = !forwardArgs.empty();
127 +
128 + // Output the command token, made optional if arguments are present.
129 + if (!commands.empty())
130 + {
131 + infoOut << ' ';
132 +
133 + if (!arguments.empty())
134 + {
135 + infoOut << L'[';
136 + }
137 +
138 + infoOut << L'<' << Localization::WSLCCLI_Command() << L'>';
139 +
140 + if (!arguments.empty())
141 + {
142 + infoOut << L']';
143 + }
144 + }
145 +
146 + // For WSLC format of command [<options>] <positional> <args | positional2..>
147 +
148 + // Add options to the usage if there are options present.
149 + if (hasOptions)
150 + {
151 + infoOut << L" [<" << Localization::WSLCCLI_Options() << L">]";
152 + }
153 +
154 + // Add arguments to the usage if there are arguments present. Positional come after
155 + // options and may be optional or required.
156 + for (const auto& arg : positionalArgs)
157 + {
158 + infoOut << L' ';
159 +
160 + if (!arg.Required())
161 + {
162 + infoOut << L'[';
163 + }
164 +
165 + infoOut << L'<' << arg.Name() << L'>';
166 +
167 + if (arg.Limit() > 1)
168 + {
169 + infoOut << L"...";
170 + }
171 +
172 + if (!arg.Required())
173 + {
174 + infoOut << L']';
175 + }
176 + }
177 +
178 + if (hasForwardArgs)
179 + {
180 + // Assume only one forward arg is present, as multiple forwards would be
181 + // ambiguous in usage. Revisit if this becomes a scenario.
182 + infoOut << L" [<" << forwardArgs.front().Name() << L">...]";
183 + }
184 +
185 + infoOut << std::endl << std::endl;
186 +
187 + if (!commandAliases.empty())
188 + {
189 + infoOut << Localization::WSLCCLI_AvailableCommandAliases() << L' ';
190 + infoOut << string::Join(commandAliases, L' ');
191 + infoOut << std::endl << std::endl;
192 + }
193 +
194 + if (!commands.empty())
195 + {
196 + if (Name() == FullName())
197 + {
198 + infoOut << Localization::WSLCCLI_AvailableCommands() << std::endl;
199 + }
200 + else
201 + {
202 + infoOut << Localization::WSLCCLI_AvailableSubcommands() << std::endl;
203 + }
204 +
205 + size_t maxCommandNameLength = 0;
206 + for (const auto& command : commands)
207 + {
208 + maxCommandNameLength = std::max(maxCommandNameLength, command->Name().length());
209 + }
210 +
211 + for (const auto& command : commands)
212 + {
213 + size_t fillChars = (maxCommandNameLength - command->Name().length()) + 2;
214 + infoOut << L" " << command->Name() << std::wstring(fillChars, L' ') << command->ShortDescription() << std::endl;
215 + }
216 +
217 + infoOut << std::endl << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L']' << std::endl;
218 + }
219 +
220 + if (!arguments.empty())
221 + {
222 + if (!commands.empty())
223 + {
224 + infoOut << std::endl;
225 + }
226 +
227 + size_t maxArgNameLength = 0;
228 + for (const auto& arg : arguments)
229 + {
230 + auto argLength = arg.GetUsageString().length();
231 + maxArgNameLength = std::max(maxArgNameLength, argLength);
232 + }
233 +
234 + if (hasArguments)
235 + {
236 + infoOut << Localization::WSLCCLI_AvailableArguments() << std::endl;
237 +
238 + for (const auto& arg : positionalArgs)
239 + {
240 + size_t fillChars = (maxArgNameLength - arg.Name().length()) + 2;
241 + infoOut << L" " << arg.Name() << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
242 + }
243 + }
244 +
245 + if (hasForwardArgs)
246 + {
247 + for (const auto& arg : forwardArgs)
248 + {
249 + size_t fillChars = (maxArgNameLength - arg.Name().length()) + 2;
250 + infoOut << L" " << arg.Name() << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
251 + }
252 + }
253 +
254 + if (hasOptions)
255 + {
256 + if (hasArguments || hasForwardArgs)
257 + {
258 + infoOut << std::endl;
259 + }
260 +
261 + infoOut << Localization::WSLCCLI_AvailableOptions() << std::endl;
262 + for (const auto& arg : standardArgs)
263 + {
264 + auto usage = arg.GetUsageString();
265 + size_t fillChars = (maxArgNameLength - usage.length()) + 2;
266 + infoOut << L" " << usage << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
267 + }
268 + }
269 + }
270 +
271 + PrintMessage(infoOut.str(), stdout);
272 +}
273 +
274 +std::unique_ptr<Command> Command::FindSubCommand(Invocation& inv) const
275 +{
276 + auto itr = inv.begin();
277 + if (itr == inv.end() || (*itr)[0] == WSLC_CLI_ARG_ID_CHAR)
278 + {
279 + // No more command arguments to check, so no command to find
280 + return {};
281 + }
282 +
283 + auto commands = GetCommands();
284 + if (commands.empty())
285 + {
286 + return {};
287 + }
288 +
289 + for (auto& command : commands)
290 + {
291 + if (string::IsEqual(*itr, command->Name()))
292 + {
293 + inv.consume(itr);
294 + return std::move(command);
295 + }
296 +
297 + for (const auto& alias : command->Aliases())
298 + {
299 + if (string::IsEqual(*itr, alias))
300 + {
301 + inv.consume(itr);
302 + return std::move(command);
303 + }
304 + }
305 + }
306 +
307 + throw CommandException(Localization::WSLCCLI_UnrecognizedCommandError(std::wstring_view{*itr}));
308 +}
309 +
310 +// Convert the invocation vector into a map of argument types and their associated values.
311 +// Argument map is based on the arguments that the command defines and are stored as
312 +// an enum -> variant multimap. This is parsing and value storage only, not validation of
313 +// the argument data.
314 +void Command::ParseArguments(Invocation& inv, ArgMap& execArgs) const
315 +{
316 + auto definedArgs = GetAllArguments();
317 +
318 + ParseArgumentsStateMachine stateMachine{inv, execArgs, std::move(definedArgs)};
319 +
320 + while (stateMachine.Step())
321 + {
322 + stateMachine.ThrowIfError();
323 + }
324 +}
325 +
326 +// Validates the ArgMap produced by ParseArguments. ArgMap is assumed to have
327 +// been populated and parsed successfully from the invocation and now we are validating
328 +// that the arguments provided meet the requirements of the command. This includes checking
329 +// that all required arguments are present and no arguments exceed their count limits.
330 +// Any defined validation for specific ArgTypes are also run.
331 +void Command::ValidateArguments(ArgMap& execArgs) const
332 +{
333 + // If help is asked for, don't bother validating anything else.
334 + if (execArgs.Contains(ArgType::Help))
335 + {
336 + return;
337 + }
338 +
339 + auto allArgs = GetAllArguments();
340 + for (const auto& arg : allArgs)
341 + {
342 + if (arg.Required() && !execArgs.Contains(arg.Type()))
343 + {
344 + throw CommandException(Localization::WSLCCLI_RequiredArgumentError(arg.Name()));
345 + }
346 +
347 + if ((arg.Limit() > 0) && (arg.Limit() < execArgs.Count(arg.Type())))
348 + {
349 + throw CommandException(Localization::WSLCCLI_TooManyArgumentsError(arg.Name()));
350 + }
351 +
352 + if (execArgs.Contains(arg.Type()))
353 + {
354 + arg.Validate(execArgs);
355 + }
356 + }
357 +
358 + ValidateArgumentsInternal(execArgs);
359 +}
360 +
361 +void Command::Execute(CLIExecutionContext& context) const
362 +{
363 + // If Help was part of the validated argument set, we will output help instead of executing.
364 + if (context.Args.Contains(ArgType::Help))
365 + {
366 + OutputHelp();
367 + }
368 + else
369 + {
370 + // Execute internal has the actual command execution path.
371 + ExecuteInternal(context);
372 + }
373 +}
374 +
375 +// External execution entry point called by the core execution flow.
376 +void Execute(CLIExecutionContext& context, std::unique_ptr<Command>& command)
377 +{
378 + command->Execute(context);
379 +}
380 +
381 +void Command::ValidateArgumentsInternal(const ArgMap&) const
382 +{
383 + // Commands may not need any extra validation; they'll override if they do.
384 +}
385 +} // namespace wsl::windows::wslc
src/windows/wslc/core/Command.h new
+105
@@ -0,0 +1,105 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + Command.h
8 +
9 +Abstract:
10 +
11 + Declaration of command class.
12 +
13 +--*/
14 +#pragma once
15 +#include "Argument.h"
16 +#include "Exceptions.h"
17 +#include "ArgumentTypes.h"
18 +#include "CLIExecutionContext.h"
19 +#include "Invocation.h"
20 +#include "ArgumentParser.h"
21 +
22 +#include <memory>
23 +#include <optional>
24 +#include <string>
25 +#include <string_view>
26 +#include <vector>
27 +
28 +using namespace wsl::windows::wslc::execution;
29 +using namespace wsl::windows::wslc::argument;
30 +
31 +namespace wsl::windows::wslc {
32 +
33 +constexpr std::wstring_view s_ExecutableName = L"wslc";
34 +
35 +struct Command
36 +{
37 + // The character used to split between commands and their parents in FullName.
38 + constexpr static wchar_t ParentSplitChar = L':';
39 +
40 + Command(std::wstring_view name, const std::wstring& parent) : Command(name, {}, parent)
41 + {
42 + }
43 + Command(std::wstring_view name, std::vector<std::wstring_view>&& aliases, const std::wstring& parent);
44 +
45 + virtual ~Command() = default;
46 +
47 + Command(const Command&) = default;
48 + Command& operator=(const Command&) = default;
49 +
50 + Command(Command&&) = default;
51 + Command& operator=(Command&&) = default;
52 +
53 + std::wstring_view Name() const
54 + {
55 + return m_name;
56 + }
57 + const std::wstring& FullName() const
58 + {
59 + return m_fullName;
60 + }
61 + const std::vector<std::wstring_view>& Aliases() const
62 + {
63 + return m_aliases;
64 + }
65 +
66 + virtual std::vector<std::unique_ptr<Command>> GetCommands() const
67 + {
68 + return {};
69 + }
70 + virtual std::vector<Argument> GetArguments() const
71 + {
72 + return {};
73 + }
74 +
75 + virtual std::vector<Argument> GetAllArguments() const
76 + {
77 + auto args = GetArguments();
78 + args.emplace_back(Argument::Create(ArgType::Help));
79 + return args;
80 + }
81 +
82 + virtual std::wstring ShortDescription() const = 0;
83 + virtual std::wstring LongDescription() const = 0;
84 +
85 + void OutputIntroHeader() const;
86 + void OutputHelp(const CommandException* exception = nullptr) const;
87 +
88 + std::unique_ptr<Command> FindSubCommand(Invocation& inv) const;
89 + void ParseArguments(Invocation& inv, ArgMap& execArgs) const;
90 + void ValidateArguments(ArgMap& execArgs) const;
91 +
92 + virtual void Execute(CLIExecutionContext& context) const;
93 +
94 +protected:
95 + virtual void ValidateArgumentsInternal(const ArgMap& execArgs) const;
96 + virtual void ExecuteInternal(CLIExecutionContext& context) const = 0;
97 +
98 +private:
99 + std::wstring_view m_name;
100 + std::vector<std::wstring_view> m_aliases;
101 + std::wstring m_fullName;
102 +};
103 +
104 +void Execute(CLIExecutionContext& context, std::unique_ptr<Command>& command);
105 +} // namespace wsl::windows::wslc
src/windows/wslc/core/Exceptions.h new
+40
@@ -0,0 +1,40 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + Exceptions.h
8 +
9 +Abstract:
10 +
11 + Header file for Exceptions.
12 +
13 +--*/
14 +#pragma once
15 +
16 +namespace wsl::windows::wslc {
17 +// Base exception for all command-related errors
18 +struct CommandException
19 +{
20 + CommandException(std::wstring_view message) : m_message(message)
21 + {
22 + }
23 +
24 + const std::wstring& Message() const
25 + {
26 + return m_message;
27 + }
28 +
29 +protected:
30 + std::wstring m_message;
31 +};
32 +
33 +// Specific exception for argument parsing errors
34 +struct ArgumentException : CommandException
35 +{
36 + ArgumentException(std::wstring_view message) : CommandException(message)
37 + {
38 + }
39 +};
40 +} // namespace wsl::windows::wslc
src/windows/wslc/core/ExecutionContextData.h new
+61
@@ -0,0 +1,61 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ExecutionContextData.h
8 +
9 +Abstract:
10 +
11 + Header file for defining execution context data mappings.
12 +
13 +--*/
14 +#pragma once
15 +#include "EnumVariantMap.h"
16 +#include "ContainerModel.h"
17 +#include "ImageModel.h"
18 +#include "SessionModel.h"
19 +#include "wslc.h"
20 +
21 +#include <string>
22 +
23 +#define DEFINE_DATA_MAPPING(_typeName_, _valueType_) \
24 + template <> \
25 + struct DataMapping<Data::_typeName_> \
26 + { \
27 + using value_t = _valueType_; \
28 + };
29 +
30 +namespace wsl::windows::wslc::execution {
31 +// Names a piece of data stored in the context by a task step.
32 +// Must start at 0 to enable direct access to variant in Context.
33 +// Max must be last and unused.
34 +enum class Data : size_t
35 +{
36 + Session,
37 + Containers,
38 + ContainerOptions,
39 + Images,
40 + Volumes,
41 +
42 + Max
43 +};
44 +
45 +namespace details {
46 + template <Data D>
47 + struct DataMapping
48 + {
49 + };
50 +
51 + DEFINE_DATA_MAPPING(Session, wsl::windows::wslc::models::Session);
52 + DEFINE_DATA_MAPPING(Containers, std::vector<wsl::windows::wslc::models::ContainerInformation>);
53 + DEFINE_DATA_MAPPING(ContainerOptions, wsl::windows::wslc::models::ContainerOptions);
54 + DEFINE_DATA_MAPPING(Images, std::vector<wsl::windows::wslc::models::ImageInformation>);
55 + DEFINE_DATA_MAPPING(Volumes, std::vector<WSLCVolumeInformation>);
56 +} // namespace details
57 +
58 +struct DataMap : wsl::windows::wslc::EnumBasedVariantMap<Data, wsl::windows::wslc::execution::details::DataMapping>
59 +{
60 +};
61 +} // namespace wsl::windows::wslc::execution
src/windows/wslc/core/Invocation.h new
+100
@@ -0,0 +1,100 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + Invocation.h
8 +
9 +Abstract:
10 +
11 + Header file for walking through and processing a command line invocation.
12 +
13 +--*/
14 +#pragma once
15 +#include <string>
16 +#include <vector>
17 +
18 +namespace wsl::windows::wslc {
19 +struct Invocation
20 +{
21 + Invocation(std::vector<std::wstring>&& args) : m_args(std::move(args))
22 + {
23 + }
24 +
25 + struct iterator
26 + {
27 + iterator(size_t arg, std::vector<std::wstring>& args) : m_arg(arg), m_args(args)
28 + {
29 + }
30 +
31 + iterator(const iterator&) = default;
32 + iterator& operator=(const iterator&) = default;
33 +
34 + iterator operator++()
35 + {
36 + return {++m_arg, m_args};
37 + }
38 + iterator operator++(int)
39 + {
40 + return {m_arg++, m_args};
41 + }
42 + iterator operator--()
43 + {
44 + return {--m_arg, m_args};
45 + }
46 + iterator operator--(int)
47 + {
48 + return {m_arg--, m_args};
49 + }
50 +
51 + bool operator==(const iterator& other) const
52 + {
53 + return m_arg == other.m_arg;
54 + }
55 + bool operator!=(const iterator& other) const
56 + {
57 + return m_arg != other.m_arg;
58 + }
59 +
60 + const std::wstring& operator*() const
61 + {
62 + return m_args[m_arg];
63 + }
64 + const std::wstring* operator->() const
65 + {
66 + return &(m_args[m_arg]);
67 + }
68 +
69 + size_t index() const
70 + {
71 + return m_arg;
72 + }
73 +
74 + private:
75 + size_t m_arg;
76 + std::vector<std::wstring>& m_args;
77 + };
78 +
79 + size_t size() const
80 + {
81 + return m_args.size();
82 + }
83 + iterator begin()
84 + {
85 + return {m_currentFirstArg, m_args};
86 + }
87 + iterator end()
88 + {
89 + return {m_args.size(), m_args};
90 + }
91 + void consume(const iterator& i)
92 + {
93 + m_currentFirstArg = i.index() + 1;
94 + }
95 +
96 +private:
97 + std::vector<std::wstring> m_args;
98 + size_t m_currentFirstArg = 0;
99 +};
100 +} // namespace wsl::windows::wslc
src/windows/wslc/core/Main.cpp new
+151
@@ -0,0 +1,151 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + Main.cpp
8 +
9 +Abstract:
10 +
11 + Main program entry point.
12 +
13 +--*/
14 +#define WIN32_LEAN_AND_MEAN
15 +#pragma once
16 +#include <Windows.h>
17 +#include "precomp.h"
18 +#include "wslutil.h"
19 +#include "Errors.h"
20 +#include "CLIExecutionContext.h"
21 +#include "Invocation.h"
22 +#include "RootCommand.h"
23 +
24 +using namespace wsl::shared;
25 +using namespace wsl::windows::common;
26 +using namespace wsl::windows::wslc::execution;
27 +
28 +namespace wsl::windows::wslc {
29 +int CoreMain(int argc, wchar_t const** argv)
30 +try
31 +{
32 + EnableContextualizedErrors(false, true);
33 + HRESULT result = S_OK;
34 +
35 + // Initialize runtime and COM.
36 + wslutil::ConfigureCrt();
37 + wslutil::InitializeWil();
38 +
39 + WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild);
40 + auto cleanupTelemetry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslTraceLoggingUninitialize(); });
41 +
42 + wslutil::SetCrtEncoding(_O_U8TEXT);
43 + auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
44 + wslutil::CoInitializeSecurity();
45 +
46 + // The execution context must be declared after COM is initialized because it stores internal
47 + // COM references.
48 + CLIExecutionContext context;
49 +
50 + // Register a console control handler so Ctrl-C signals the cancel event.
51 + // This allows long-running operations (e.g. image build) to be cancelled.
52 + // The static pointer is required because SetConsoleCtrlHandler only accepts function pointers.
53 + // CancelEvent starts null; when a task creates it, the handler picks it up automatically.
54 + static auto& s_cancelEvent = context.CancelEvent;
55 + auto ctrlHandler = [](DWORD ctrlType) -> BOOL {
56 + if (ctrlType == CTRL_C_EVENT || ctrlType == CTRL_BREAK_EVENT)
57 + {
58 + if (s_cancelEvent && !s_cancelEvent.is_signaled())
59 + {
60 + s_cancelEvent.SetEvent();
61 + return TRUE;
62 + }
63 + }
64 + return FALSE;
65 + };
66 + SetConsoleCtrlHandler(ctrlHandler, TRUE);
67 + auto unregisterHandler = wil::scope_exit([&]() { SetConsoleCtrlHandler(ctrlHandler, FALSE); });
68 +
69 + WSADATA data{};
70 + THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &data));
71 + auto wsaCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WSACleanup(); });
72 +
73 + std::unique_ptr<Command> command = std::make_unique<RootCommand>();
74 +
75 + try
76 + {
77 + std::vector<std::wstring> args;
78 + for (int i = 1; i < argc; ++i)
79 + {
80 + args.emplace_back(argv[i]);
81 + }
82 +
83 + Invocation invocation{std::move(args)};
84 + std::unique_ptr<Command> subCommand = command->FindSubCommand(invocation);
85 + while (subCommand)
86 + {
87 + command = std::move(subCommand);
88 + subCommand = command->FindSubCommand(invocation);
89 + }
90 +
91 + command->ParseArguments(invocation, context.Args);
92 + command->ValidateArguments(context.Args);
93 + command->Execute(context);
94 + }
95 + // Exceptions specific to parsing the arguments of a command
96 + catch (const CommandException& ce)
97 + {
98 + // A command exception means there was an input failure. Display the help
99 + // along with the error message to help the user correct their input.
100 + command->OutputHelp(&ce);
101 + return 1;
102 + }
103 + // Any other type of error unrelated to the command parsing.
104 + catch (...)
105 + {
106 + LOG_CAUGHT_EXCEPTION();
107 +
108 + // Using WSL shared utility to get the HRESULT from the caught exception.
109 + // CLIExecutionContext is a derived class of wsl::windows::common::ExecutionContext.
110 + result = wil::ResultFromCaughtException();
111 +
112 + // If the user pressed Ctrl-C, acknowledge the cancellation and exit.
113 + if (context.CancelEvent && context.CancelEvent.is_signaled())
114 + {
115 + fwprintf(stderr, L"\nCancelled.\n");
116 + return 1;
117 + }
118 +
119 + if (FAILED(result))
120 + {
121 + if (const auto& reported = context.ReportedError())
122 + {
123 + auto strings = wslutil::ErrorToString(*reported);
124 + auto errorMessage = strings.Message.empty() ? strings.Code : strings.Message;
125 + wslutil::PrintMessage(Localization::MessageErrorCode(errorMessage, wslutil::ErrorCodeToString(result)), stderr);
126 + }
127 + else
128 + {
129 + // Fallback for errors without context
130 + wslutil::PrintMessage(Localization::MessageErrorCode("", wslutil::ErrorCodeToString(result)), stderr);
131 + }
132 + }
133 + }
134 +
135 + if (context.ExitCode.has_value())
136 + {
137 + return context.ExitCode.value();
138 + }
139 +
140 + return FAILED(result) ? 1 : 0;
141 +}
142 +catch (...)
143 +{
144 + return 1;
145 +}
146 +} // namespace wsl::windows::wslc
147 +
148 +int wmain(int argc, wchar_t const** argv)
149 +{
150 + return wsl::windows::wslc::CoreMain(argc, argv);
151 +}
src/windows/wslc/core/TableOutput.h new
+477
@@ -0,0 +1,477 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + TableOutput.h
8 +
9 +Abstract:
10 +
11 + Header file for outputting data in a table format.
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include <algorithm>
17 +#include <array>
18 +#include <cwchar>
19 +#include <functional>
20 +#include <sstream>
21 +#include <string>
22 +#include <utility>
23 +#include <vector>
24 +#include <wslutil.h>
25 +
26 +namespace wsl::windows::wslc {
27 +
28 +namespace detail {
29 + // This function outputs a table line.
30 + inline void PrintTableLine(const std::wstring& line, FILE* stream)
31 + {
32 + ::wsl::windows::common::wslutil::PrintMessage(line, stream);
33 + }
34 +} // namespace detail
35 +
36 +// Helper function to get display width of a string
37 +// For now, uses simple length (can be enhanced with proper Unicode width calculation)
38 +inline size_t GetStringColumnWidth(const wchar_t* str)
39 +{
40 + if (!str)
41 + {
42 + return 0;
43 + }
44 + return wcslen(str);
45 +}
46 +
47 +// Helper function to trim string to a specific column width
48 +inline std::wstring TrimStringToColumnWidth(const wchar_t* str, size_t maxWidth, size_t& actualWidth)
49 +{
50 + if (!str)
51 + {
52 + actualWidth = 0;
53 + return L"";
54 + }
55 +
56 + size_t len = wcslen(str);
57 + if (len <= maxWidth)
58 + {
59 + actualWidth = len;
60 + return std::wstring(str);
61 + }
62 +
63 + actualWidth = maxWidth;
64 + return std::wstring(str, maxWidth);
65 +}
66 +
67 +// Column width configuration options
68 +struct ColumnWidthConfig
69 +{
70 + static constexpr size_t NoLimit = 0;
71 +
72 + size_t MinWidth = NoLimit; // Minimum column width (NoLimit = use header width)
73 + size_t MaxWidth = NoLimit; // Maximum column width (NoLimit = unlimited)
74 + bool PreferredShrink = true; // Should this column shrink first when space is limited?
75 +};
76 +
77 +// Column definition with name and configuration
78 +struct ColumnDefinition
79 +{
80 + std::wstring Name;
81 + ColumnWidthConfig Config;
82 +};
83 +
84 +// Enables output data in a table format.
85 +// TODO: Improve for use with sparse data.
86 +template <size_t FieldCount>
87 +struct TableOutput
88 +{
89 + static_assert(FieldCount > 0, "TableOutput requires at least one column");
90 +
91 + using header_t = std::array<std::wstring, FieldCount>;
92 + using line_t = std::array<std::wstring, FieldCount>;
93 + using column_config_t = std::array<ColumnWidthConfig, FieldCount>;
94 + using column_def_t = std::array<ColumnDefinition, FieldCount>;
95 + using OutputFn = std::function<void(const std::wstring&)>;
96 +
97 + static constexpr size_t DefaultColumnPadding = 3; // Docker-like spacing between columns
98 +
99 + // Constructor with default behavior (no column limits)
100 + TableOutput(header_t&& header, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
101 + m_sizingBuffer(sizingBuffer), m_limitColumnWidths(false), m_columnPadding(columnPadding), m_outputFn(DefaultOutputFn())
102 + {
103 + InitializeColumns(std::move(header));
104 + }
105 +
106 + // Constructor with column width configuration (legacy)
107 + TableOutput(header_t&& header, column_config_t&& config, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
108 + m_sizingBuffer(sizingBuffer),
109 + m_limitColumnWidths(true),
110 + m_columnPadding(columnPadding),
111 + m_columnConfigs(std::move(config)),
112 + m_outputFn(DefaultOutputFn())
113 + {
114 + InitializeColumns(std::move(header));
115 + }
116 +
117 + // Constructor with column definitions (name + config together)
118 + TableOutput(column_def_t&& columns, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
119 + m_sizingBuffer(sizingBuffer), m_limitColumnWidths(true), m_columnPadding(columnPadding), m_outputFn(DefaultOutputFn())
120 + {
121 + header_t headers;
122 + for (size_t i = 0; i < FieldCount; ++i)
123 + {
124 + headers[i] = std::move(columns[i].Name);
125 + m_columnConfigs[i] = columns[i].Config;
126 + }
127 + InitializeColumns(std::move(headers));
128 + }
129 +
130 + // Enable/disable column width limiting
131 + void SetColumnWidthLimiting(bool enable)
132 + {
133 + m_limitColumnWidths = enable;
134 + }
135 +
136 + // Set configuration for a specific column
137 + void SetColumnConfig(size_t columnIndex, const ColumnWidthConfig& config)
138 + {
139 + if (columnIndex < FieldCount)
140 + {
141 + m_columnConfigs[columnIndex] = config;
142 + }
143 + }
144 +
145 + // Set whether to always show header even when there are no rows
146 + void SetAlwaysShowHeader(bool alwaysShow)
147 + {
148 + m_alwaysShowHeader = alwaysShow;
149 + }
150 +
151 + // Set whether to show the header row
152 + void SetShowHeader(bool showHeader)
153 + {
154 + m_showHeader = showHeader;
155 + }
156 +
157 + // Override the output function (e.g. redirect to a stringstream in tests).
158 + void SetOutputFunction(OutputFn fn)
159 + {
160 + FAIL_FAST_IF_MSG(!fn, "OutputFn must not be empty");
161 + m_outputFn = std::move(fn);
162 + }
163 +
164 + // Override the console width used for column shrinking (useful in tests).
165 + // Pass 0 to restore the default behaviour (query the real console).
166 + void SetConsoleWidthOverride(size_t width)
167 + {
168 + m_consoleWidthOverride = width;
169 + }
170 +
171 + void OutputLine(line_t&& line)
172 + {
173 + m_empty = false;
174 +
175 + // When width limiting is disabled, buffer all rows to ensure accurate column sizing
176 + // and prevent truncation (e.g., for --no-trunc flag)
177 + if (!m_limitColumnWidths || m_buffer.size() < m_sizingBuffer)
178 + {
179 + m_buffer.emplace_back(std::move(line));
180 + }
181 + else
182 + {
183 + EvaluateAndFlushBuffer();
184 + OutputLineToStream(line);
185 + }
186 + }
187 +
188 + void Complete()
189 + {
190 + if (!m_empty)
191 + {
192 + EvaluateAndFlushBuffer();
193 + }
194 + else if (m_alwaysShowHeader && m_showHeader)
195 + {
196 + OutputHeaderOnly();
197 + }
198 + }
199 +
200 + bool IsEmpty()
201 + {
202 + return m_empty;
203 + }
204 +
205 +private:
206 + // A column in the table.
207 + struct Column
208 + {
209 + std::wstring Name;
210 + size_t MinLength = 0;
211 + size_t MaxLength = 0;
212 + size_t ConfiguredMaxLength = 0; // Max length from configuration
213 + bool SpaceAfter = true;
214 + };
215 +
216 + std::array<Column, FieldCount> m_columns;
217 + column_config_t m_columnConfigs;
218 + size_t m_sizingBuffer;
219 + size_t m_columnPadding;
220 + std::vector<line_t> m_buffer;
221 + bool m_bufferEvaluated = false;
222 + bool m_empty = true;
223 + bool m_limitColumnWidths = false;
224 + bool m_alwaysShowHeader = true;
225 + bool m_showHeader = true;
226 + std::wstringstream m_stream;
227 + OutputFn m_outputFn;
228 + size_t m_consoleWidthOverride = 0;
229 +
230 + static OutputFn DefaultOutputFn()
231 + {
232 + return [](const std::wstring& line) { detail::PrintTableLine(line, stdout); };
233 + }
234 +
235 + void InitializeColumns(header_t&& header)
236 + {
237 + for (size_t i = 0; i < FieldCount; ++i)
238 + {
239 + m_columns[i].Name = std::move(header[i]);
240 + m_columns[i].MinLength = GetStringColumnWidth(m_columns[i].Name.c_str());
241 + m_columns[i].MaxLength = 0;
242 +
243 + // Apply configured max width if limiting is enabled
244 + if (m_limitColumnWidths && m_columnConfigs[i].MaxWidth != ColumnWidthConfig::NoLimit)
245 + {
246 + m_columns[i].ConfiguredMaxLength = m_columnConfigs[i].MaxWidth;
247 + }
248 +
249 + // Apply configured min width
250 + if (m_columnConfigs[i].MinWidth != ColumnWidthConfig::NoLimit)
251 + {
252 + m_columns[i].MinLength = std::max(m_columns[i].MinLength, m_columnConfigs[i].MinWidth);
253 + }
254 + }
255 + }
256 +
257 + size_t GetConsoleWidth()
258 + {
259 + if (m_consoleWidthOverride > 0)
260 + {
261 + return m_consoleWidthOverride;
262 + }
263 +
264 + CONSOLE_SCREEN_BUFFER_INFO consoleInfo{};
265 + HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
266 +
267 + if (GetConsoleScreenBufferInfo(hConsole, &consoleInfo))
268 + {
269 + return static_cast<size_t>(consoleInfo.srWindow.Right - consoleInfo.srWindow.Left + 1);
270 + }
271 +
272 + // Default to 80 columns if console info is unavailable
273 + return 80;
274 + }
275 +
276 + void OutputHeaderOnly()
277 + {
278 + // Set MaxLength to MinLength for all columns (header width only)
279 + for (size_t i = 0; i < FieldCount; ++i)
280 + {
281 + m_columns[i].MaxLength = m_columns[i].MinLength;
282 + }
283 +
284 + // Set spacing configuration
285 + m_columns[FieldCount - 1].SpaceAfter = false;
286 +
287 + // Output the header
288 + line_t headerLine;
289 + for (size_t i = 0; i < FieldCount; ++i)
290 + {
291 + headerLine[i] = m_columns[i].Name.c_str();
292 + }
293 +
294 + OutputLineToStream(headerLine);
295 + m_bufferEvaluated = true;
296 + }
297 +
298 + void EvaluateAndFlushBuffer()
299 + {
300 + if (m_bufferEvaluated)
301 + {
302 + return;
303 + }
304 +
305 + // Determine the maximum length for all columns
306 + for (const auto& line : m_buffer)
307 + {
308 + for (size_t i = 0; i < FieldCount; ++i)
309 + {
310 + size_t columnWidth = GetStringColumnWidth(line[i].c_str());
311 +
312 + // Apply configured max width if limiting is enabled
313 + if (m_limitColumnWidths && m_columns[i].ConfiguredMaxLength != ColumnWidthConfig::NoLimit)
314 + {
315 + columnWidth = std::min(columnWidth, m_columns[i].ConfiguredMaxLength);
316 + }
317 +
318 + m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, columnWidth);
319 + }
320 + }
321 +
322 + // If there are actually columns with data, then also bring in the minimum size
323 + for (size_t i = 0; i < FieldCount; ++i)
324 + {
325 + if (m_columns[i].MaxLength)
326 + {
327 + m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, m_columns[i].MinLength);
328 + }
329 + }
330 +
331 + // Only output the extra space if:
332 + // 1. Not the last field
333 + m_columns[FieldCount - 1].SpaceAfter = false;
334 +
335 + // 2. Not empty (taken care of by not doing anything if empty)
336 + // 3. There are non-empty fields after
337 + for (size_t i = FieldCount - 1; i > 0; --i)
338 + {
339 + if (m_columns[i].MaxLength)
340 + {
341 + break;
342 + }
343 + else
344 + {
345 + m_columns[i - 1].SpaceAfter = false;
346 + }
347 + }
348 +
349 + // Determine the total width required to not truncate any columns
350 + size_t totalRequired = 0;
351 +
352 + for (size_t i = 0; i < FieldCount; ++i)
353 + {
354 + totalRequired += m_columns[i].MaxLength + (m_columns[i].SpaceAfter ? m_columnPadding : 0);
355 + }
356 +
357 + // Only apply console width constraints if m_limitColumnWidths is true
358 + if (m_limitColumnWidths)
359 + {
360 + size_t consoleWidth = GetConsoleWidth();
361 +
362 + // If the total space would be too big, shrink them.
363 + // We don't want to use the last column, lest we auto-wrap
364 + if (totalRequired >= consoleWidth)
365 + {
366 + size_t extra = (totalRequired - consoleWidth) + 1;
367 +
368 + while (extra > 0)
369 + {
370 + // Find the largest shrinkable column
371 + size_t targetIndex = 0;
372 + size_t targetVal = 0;
373 +
374 + for (size_t j = 0; j < FieldCount; ++j)
375 + {
376 + // Skip columns at or below minimum
377 + if (m_columns[j].MaxLength <= m_columns[j].MinLength)
378 + {
379 + continue;
380 + }
381 +
382 + // Prefer columns marked as preferredShrink
383 + bool isPreferredShrink = m_columnConfigs[j].PreferredShrink;
384 + bool currentIsPreferred = m_columnConfigs[targetIndex].PreferredShrink;
385 +
386 + if (isPreferredShrink && !currentIsPreferred)
387 + {
388 + targetIndex = j;
389 + targetVal = m_columns[j].MaxLength;
390 + }
391 + else if (isPreferredShrink == currentIsPreferred && m_columns[j].MaxLength > targetVal)
392 + {
393 + targetIndex = j;
394 + targetVal = m_columns[j].MaxLength;
395 + }
396 + }
397 +
398 + // If no shrinkable column found, break
399 + if (targetVal == 0)
400 + {
401 + break;
402 + }
403 +
404 + m_columns[targetIndex].MaxLength -= 1;
405 + extra -= 1;
406 + }
407 +
408 + totalRequired = std::min(totalRequired, consoleWidth - 1);
409 + }
410 + }
411 +
412 + if (m_showHeader)
413 + {
414 + line_t headerLine;
415 + for (size_t i = 0; i < FieldCount; ++i)
416 + {
417 + headerLine[i] = m_columns[i].Name.c_str();
418 + }
419 +
420 + OutputLineToStream(headerLine);
421 + }
422 +
423 + for (const auto& line : m_buffer)
424 + {
425 + OutputLineToStream(line);
426 + }
427 +
428 + m_bufferEvaluated = true;
429 + }
430 +
431 + void OutputLineToStream(const line_t& line)
432 + {
433 + for (size_t i = 0; i < FieldCount; ++i)
434 + {
435 + const auto& col = m_columns[i];
436 +
437 + if (col.MaxLength)
438 + {
439 + size_t valueLength = GetStringColumnWidth(line[i].c_str());
440 +
441 + if (valueLength > col.MaxLength)
442 + {
443 + size_t actualWidth;
444 + m_stream << TrimStringToColumnWidth(line[i].c_str(), col.MaxLength - 1, actualWidth) << L"\u2026"; // Unicode ellipsis character
445 +
446 + // Some characters take 2 unit space, the trimmed string length might be 1 less than the expected length.
447 + if (actualWidth != col.MaxLength - 1)
448 + {
449 + m_stream << L' ';
450 + }
451 +
452 + if (col.SpaceAfter)
453 + {
454 + m_stream << std::wstring(m_columnPadding, L' ');
455 + }
456 + }
457 + else
458 + {
459 + m_stream << line[i];
460 +
461 + if (col.SpaceAfter)
462 + {
463 + m_stream << std::wstring(col.MaxLength - valueLength + m_columnPadding, L' ');
464 + }
465 + }
466 + }
467 + }
468 +
469 + const std::wstring rendered = m_stream.str();
470 + m_stream.str(L"");
471 + m_stream.clear();
472 +
473 + m_outputFn(rendered);
474 + }
475 +};
476 +
477 +} // namespace wsl::windows::wslc
src/windows/wslc/services/BuildImageCallback.cpp new
+29
@@ -0,0 +1,29 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + BuildImageCallback.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the BuildImageCallback Implementation.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "BuildImageCallback.h"
17 +
18 +namespace wsl::windows::wslc::services {
19 +HRESULT BuildImageCallback::OnProgress(LPCSTR status, LPCSTR /*id*/, ULONGLONG /*current*/, ULONGLONG /*total*/)
20 +try
21 +{
22 + if (status != nullptr && *status != '\0')
23 + {
24 + wprintf(L"%hs", status);
25 + }
26 + return S_OK;
27 +}
28 +CATCH_RETURN();
29 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/BuildImageCallback.h new
+24
@@ -0,0 +1,24 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + BuildImageCallback.h
8 +
9 +Abstract:
10 +
11 + This file contains the BuildImageCallback definition
12 +
13 +--*/
14 +#pragma once
15 +#include "SessionService.h"
16 +
17 +namespace wsl::windows::wslc::services {
18 +class DECLSPEC_UUID("3EDD5DBF-CA6C-4CF7-923A-AD94B6A732E5") BuildImageCallback
19 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback, IFastRundown>
20 +{
21 +public:
22 + HRESULT OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) override;
23 +};
24 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ConsoleService.cpp new
+134
@@ -0,0 +1,134 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ConsoleService.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the ConsoleService implementation
12 +
13 +--*/
14 +#include <precomp.h>
15 +#include <WSLCProcessLauncher.h>
16 +#include "ConsoleService.h"
17 +
18 +namespace wsl::windows::wslc::services {
19 +
20 +using wsl::windows::common::ClientRunningWSLCProcess;
21 +using wsl::windows::common::relay::ReadHandle;
22 +using wsl::windows::common::relay::RelayHandle;
23 +
24 +bool ConsoleService::RelayInteractiveTty(ClientRunningWSLCProcess& Process, HANDLE Tty, bool triggerRefresh)
25 +{
26 + // Configure console for interactive usage.
27 + wsl::windows::common::ConsoleState console;
28 +
29 + if (triggerRefresh)
30 + {
31 + // In the case of an Attach, force a terminal resize to force the tty to refresh its display.
32 + // The docker client uses the same trick.
33 +
34 + auto size = console.GetWindowSize();
35 +
36 + LOG_IF_FAILED(Process.Get().ResizeTty(size.Y + 1, size.X + 1));
37 + LOG_IF_FAILED(Process.Get().ResizeTty(size.Y, size.X));
38 + }
39 +
40 + wil::unique_event exitEvent(wil::EventOptions::ManualReset);
41 +
42 + bool completed = false;
43 +
44 + // Create a thread to relay stdin to the pipe.
45 + std::thread inputThread([&]() {
46 + auto updateTerminal = [&console, &Process]() {
47 + const auto windowSize = console.GetWindowSize();
48 + LOG_IF_FAILED(Process.Get().ResizeTty(windowSize.Y, windowSize.X));
49 + };
50 +
51 + // TODO: Make this configurable (default to ctrl-p, ctrl-q).
52 + std::vector<char> detachSequence{0x10, 0x11};
53 +
54 + completed = wsl::windows::common::relay::StandardInputRelay(
55 + GetStdHandle(STD_INPUT_HANDLE), Tty, updateTerminal, exitEvent.get(), detachSequence);
56 + });
57 +
58 + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
59 + exitEvent.SetEvent();
60 + inputThread.join();
61 + });
62 +
63 + // Relay the contents of the pipe to stdout.
64 + wsl::windows::common::relay::InterruptableRelay(Tty, GetStdHandle(STD_OUTPUT_HANDLE), exitEvent.get());
65 +
66 + joinThread.reset();
67 +
68 + return completed;
69 +}
70 +
71 +void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr)
72 +{
73 + wsl::windows::common::relay::MultiHandleWait io;
74 +
75 + // Create a thread to relay stdin to the pipe.
76 + wil::unique_event exitEvent(wil::EventOptions::ManualReset);
77 +
78 + std::thread inputThread;
79 +
80 + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
81 + if (inputThread.joinable())
82 + {
83 + exitEvent.SetEvent();
84 + inputThread.join();
85 + }
86 + });
87 +
88 + if (Stdin.is_valid())
89 + {
90 + // Required because ReadFile() blocks if stdin doesn't support overlapped IO.
91 + // This can create pipe deadlocks if we get blocked reading stdin while data is available on stdout / stderr.
92 + // TODO: Will output CR instead of LF's which can confuse the linux app.
93 + // Consider a custom relay logic to fix this.
94 + inputThread = std::thread{[&]() {
95 + try
96 + {
97 + wsl::windows::common::relay::InterruptableRelay(GetStdHandle(STD_INPUT_HANDLE), Stdin.get(), exitEvent.get());
98 + }
99 + CATCH_LOG();
100 +
101 + Stdin.reset();
102 + }};
103 + }
104 +
105 + io.AddHandle(std::make_unique<RelayHandle<ReadHandle>>(std::move(Stdout), GetStdHandle(STD_OUTPUT_HANDLE)));
106 + io.AddHandle(std::make_unique<RelayHandle<ReadHandle>>(std::move(Stderr), GetStdHandle(STD_ERROR_HANDLE)));
107 +
108 + io.Run({});
109 +}
110 +
111 +int ConsoleService::AttachToCurrentConsole(wsl::windows::common::ClientRunningWSLCProcess&& process)
112 +{
113 + if (WI_IsFlagSet(process.Flags(), WSLCProcessFlagsTty))
114 + {
115 + if (!RelayInteractiveTty(process, process.GetStdHandle(WSLCFDTty).get()))
116 + {
117 + wsl::windows::common::wslutil::PrintMessage(L"[detached]", stderr);
118 + return 0;
119 + }
120 + }
121 + else
122 + {
123 + wil::unique_handle stdinHandle;
124 + if (WI_IsFlagSet(process.Flags(), WSLCProcessFlagsStdin))
125 + {
126 + stdinHandle = process.GetStdHandle(WSLCFDStdin);
127 + }
128 +
129 + RelayNonTtyProcess(std::move(stdinHandle), process.GetStdHandle(WSLCFDStdout), process.GetStdHandle(WSLCFDStderr));
130 + }
131 +
132 + return process.Wait();
133 +}
134 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ConsoleService.h new
+27
@@ -0,0 +1,27 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ConsoleService.h
8 +
9 +Abstract:
10 +
11 + This file contains the ConsoleService definition
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include <wslc.h>
17 +#include <WSLCContainerLauncher.h>
18 +
19 +namespace wsl::windows::wslc::services {
20 +class ConsoleService
21 +{
22 +public:
23 + static int AttachToCurrentConsole(wsl::windows::common::ClientRunningWSLCProcess&& process);
24 + static bool RelayInteractiveTty(wsl::windows::common::ClientRunningWSLCProcess& process, HANDLE tty, bool triggerRefresh = false);
25 + static void RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr);
26 +};
27 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ContainerModel.cpp new
+335
@@ -0,0 +1,335 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerModel.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the ContainerModel implementation
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "ContainerModel.h"
16 +
17 +namespace wsl::windows::wslc::models {
18 +
19 +using namespace wsl::shared;
20 +using namespace wsl::shared::string;
21 +
22 +PublishPort::PortRange PublishPort::PortRange::ParsePortPart(const std::string& portPart)
23 +{
24 + static auto parsePort = [](const std::string& value, const std::string& errorMessage) -> uint16_t {
25 + try
26 + {
27 + // Ensure the value is not empty and contains only digits before parsing
28 + if (value.empty() || !std::all_of(value.begin(), value.end(), ::isdigit))
29 + {
30 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
31 + }
32 +
33 + // Parse the port number and validate the range
34 + auto port = std::stoul(value, nullptr, 10);
35 + if (!PublishPort::IsValidPort(port))
36 + {
37 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
38 + }
39 + return static_cast<uint16_t>(port);
40 + }
41 + catch (const std::exception&)
42 + {
43 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
44 + }
45 + };
46 +
47 + // Find optional port range separator
48 + auto dashPos = portPart.find('-');
49 + if (dashPos != std::string::npos)
50 + {
51 + // Port range specified
52 + auto startPortStr = portPart.substr(0, dashPos);
53 + auto endPortStr = portPart.substr(dashPos + 1);
54 + auto startPort = parsePort(startPortStr, std::format("Invalid port range specified in port mapping: '{}'.", portPart));
55 + auto endPort = parsePort(endPortStr, std::format("Invalid port range specified in port mapping: '{}'.", portPart));
56 + return {startPort, endPort};
57 + }
58 +
59 + // Single port specified
60 + auto port = parsePort(portPart, std::format("Invalid port specified in port mapping: '{}'.", portPart));
61 + return {port, port};
62 +}
63 +
64 +PublishPort PublishPort::Parse(const std::string& value)
65 +{
66 + PublishPort result{};
67 + result.m_original = value;
68 +
69 + // 1. Strip optional protocol suffix
70 + std::string portPart = value;
71 + auto slashPos = value.find('/');
72 + if (slashPos != std::string::npos)
73 + {
74 + portPart = value.substr(0, slashPos);
75 + auto protocolPart = value.substr(slashPos + 1);
76 + if (protocolPart == "tcp")
77 + {
78 + result.m_protocol = PublishPort::Protocol::TCP;
79 + }
80 + else if (protocolPart == "udp")
81 + {
82 + result.m_protocol = PublishPort::Protocol::UDP;
83 + }
84 + else
85 + {
86 + THROW_HR_WITH_USER_ERROR(
87 + E_INVALIDARG, "Invalid protocol specified in port mapping. Only 'tcp' and 'udp' are supported.");
88 + }
89 + }
90 +
91 + // 2. Split off the container port from the right
92 + auto colonPos = portPart.rfind(':');
93 + std::optional<std::string> hostPortPart;
94 + if (colonPos != std::string::npos)
95 + {
96 + result.m_containerPort = PublishPort::PortRange::ParsePortPart(portPart.substr(colonPos + 1));
97 + hostPortPart = portPart.substr(0, colonPos);
98 + }
99 + else
100 + {
101 + result.m_containerPort = PublishPort::PortRange::ParsePortPart(portPart);
102 + }
103 +
104 + // 3. Parse the host port
105 + if (hostPortPart.has_value())
106 + {
107 + auto colonPos = hostPortPart->rfind(':');
108 + if (colonPos != std::string::npos)
109 + {
110 + result.m_hostIP = PublishPort::IPAddress(hostPortPart->substr(0, colonPos));
111 + auto hostPort = hostPortPart->substr(colonPos + 1);
112 + if (!hostPort.empty())
113 + {
114 + result.m_hostPort = PublishPort::PortRange::ParsePortPart(hostPort);
115 + }
116 + }
117 + else
118 + {
119 + result.m_hostPort = PublishPort::PortRange::ParsePortPart(*hostPortPart);
120 + }
121 + }
122 +
123 + result.Validate();
124 + return result;
125 +}
126 +
127 +void PublishPort::Validate() const
128 +{
129 + if (m_containerPort.Count() == 0)
130 + {
131 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, "Container port must specify at least one port.");
132 + }
133 +
134 + if (!m_containerPort.IsValid())
135 + {
136 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, "Container port must be a valid port number (1-65535).");
137 + }
138 +
139 + if (!m_hostPort.IsEphemeral())
140 + {
141 + if (!m_hostPort.IsValid())
142 + {
143 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, "Host port must be a valid port number (1-65535).");
144 + }
145 +
146 + if (m_hostPort.Count() != m_containerPort.Count())
147 + {
148 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, "Host port range must match the container port range.");
149 + }
150 + }
151 +}
152 +
153 +// Returns true if the given string is a valid Docker named volume name.
154 +// Based on Docker's named volume validation: ^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,}$
155 +// Source: https://github.com/moby/moby/blob/master/volume/validate.go
156 +bool VolumeMount::IsValidNamedVolumeName(const std::wstring& name)
157 +{
158 + static const std::wregex namedVolumeRegex(LR"(^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,}$)");
159 + return std::regex_match(name, namedVolumeRegex);
160 +}
161 +
162 +VolumeMount VolumeMount::Parse(const std::wstring& value)
163 +{
164 + auto lastColon = value.rfind(':');
165 + if (lastColon == std::wstring::npos)
166 + {
167 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeInvalidSpec(value, Localization::WSLCCLI_VolumeFormatUsage()));
168 + }
169 +
170 + VolumeMount vm;
171 + auto splitColon = lastColon;
172 + const auto lastToken = value.substr(lastColon + 1);
173 + if (IsValidMode(lastToken))
174 + {
175 + vm.m_isReadOnlyMode = IsReadOnlyMode(lastToken);
176 + if (lastColon == 0)
177 + {
178 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeInvalidSpec(value, Localization::WSLCCLI_VolumeFormatUsage()));
179 + }
180 +
181 + splitColon = value.rfind(':', lastColon - 1);
182 + if (splitColon == std::wstring::npos)
183 + {
184 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeInvalidSpec(value, Localization::WSLCCLI_VolumeFormatUsage()));
185 + }
186 +
187 + vm.m_containerPath = WideToMultiByte(value.substr(splitColon + 1, lastColon - splitColon - 1));
188 + }
189 + else
190 + {
191 + vm.m_containerPath = WideToMultiByte(lastToken);
192 + }
193 +
194 + if (vm.m_containerPath.empty())
195 + {
196 + THROW_HR_WITH_USER_ERROR(
197 + E_INVALIDARG, Localization::WSLCCLI_VolumeContainerPathEmpty(value, Localization::WSLCCLI_VolumeFormatUsage()));
198 + }
199 +
200 + if (vm.m_containerPath[0] != '/')
201 + {
202 + THROW_HR_WITH_USER_ERROR(
203 + E_INVALIDARG, Localization::WSLCCLI_VolumeContainerPathNotAbsolute(value, Localization::WSLCCLI_VolumeFormatUsage()));
204 + }
205 +
206 + const auto rawHostPath = value.substr(0, splitColon);
207 + if (rawHostPath.empty())
208 + {
209 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeHostPathEmpty(value, Localization::WSLCCLI_VolumeFormatUsage()));
210 + }
211 +
212 + // This is where we need to check if the user is referencing a named volume.
213 + // This can be either an existing named volume or a new named volume that will be created.
214 + if (VolumeMount::IsValidNamedVolumeName(rawHostPath))
215 + {
216 + vm.m_isNamedVolume = true;
217 + vm.m_host = rawHostPath;
218 + }
219 + else
220 + {
221 + // Not a named volume, so it must be a path.
222 + // Use wil::GetFullPathNameW to resolve relative paths against the CWD.
223 + std::wstring resolvedHostPath;
224 + const auto hr = wil::GetFullPathNameW(rawHostPath.c_str(), resolvedHostPath);
225 + if (FAILED(hr))
226 + {
227 + THROW_HR_WITH_USER_ERROR(hr, Localization::WSLCCLI_VolumeHostPathInvalid(value, rawHostPath));
228 + }
229 +
230 + // GetFileAttributesW validates the resolved path syntax without requiring existence.
231 + // ERROR_INVALID_NAME indicates illegal characters in the path (e.g. ":" as a component).
232 + if (GetFileAttributesW(resolvedHostPath.c_str()) == INVALID_FILE_ATTRIBUTES && GetLastError() == ERROR_INVALID_NAME)
233 + {
234 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeHostPathInvalid(value, rawHostPath));
235 + }
236 +
237 + vm.m_host = std::move(resolvedHostPath);
238 + }
239 +
240 + return vm;
241 +}
242 +
243 +std::optional<std::wstring> EnvironmentVariable::Parse(const std::wstring& entry)
244 +{
245 + if (entry.empty() || std::all_of(entry.begin(), entry.end(), std::iswspace))
246 + {
247 + return std::nullopt;
248 + }
249 +
250 + std::wstring key;
251 + std::optional<std::wstring> value;
252 +
253 + auto delimiterPos = entry.find('=');
254 + if (delimiterPos == std::wstring::npos)
255 + {
256 + key = entry;
257 + }
258 + else
259 + {
260 + key = entry.substr(0, delimiterPos);
261 + value = entry.substr(delimiterPos + 1);
262 + }
263 +
264 + if (key.empty())
265 + {
266 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_EnvKeyEmptyError());
267 + }
268 +
269 + if (std::any_of(key.begin(), key.end(), std::iswspace))
270 + {
271 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_EnvKeyWhitespaceError(key));
272 + }
273 +
274 + if (!value.has_value())
275 + {
276 + std::wstring envValue;
277 + auto hr = wil::GetEnvironmentVariableW(key.c_str(), envValue);
278 + if (FAILED(hr))
279 + {
280 + return std::nullopt;
281 + }
282 +
283 + value = envValue;
284 + }
285 +
286 + return std::format(L"{}={}", key, value.value());
287 +}
288 +
289 +std::vector<std::wstring> EnvironmentVariable::ParseFile(const std::wstring& filePath)
290 +{
291 + std::ifstream file(filePath);
292 + if (!file.is_open() || !file.good())
293 + {
294 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, std::format(L"Environment file '{}' cannot be opened for reading", filePath));
295 + }
296 +
297 + // Read the file line by line
298 + std::vector<std::wstring> envVars;
299 + std::string line;
300 + while (std::getline(file, line))
301 + {
302 + // Remove leading whitespace
303 + line.erase(line.begin(), std::find_if(line.begin(), line.end(), [](unsigned char ch) { return !std::isspace(ch); }));
304 +
305 + // Skip empty lines and comments
306 + if (line.empty() || line[0] == '#')
307 + {
308 + continue;
309 + }
310 +
311 + auto envVar = Parse(wsl::shared::string::MultiByteToWide(line));
312 + if (envVar.has_value())
313 + {
314 + envVars.push_back(std::move(envVar.value()));
315 + }
316 + }
317 +
318 + return envVars;
319 +}
320 +
321 +TmpfsMount TmpfsMount::Parse(const std::string& value)
322 +{
323 + TmpfsMount result{};
324 + auto colonPos = value.find(':');
325 + if (colonPos == std::string::npos)
326 + {
327 + result.m_containerPath = value;
328 + return result;
329 + }
330 +
331 + result.m_containerPath = value.substr(0, colonPos);
332 + result.m_options = value.substr(colonPos + 1);
333 + return result;
334 +}
335 +} // namespace wsl::windows::wslc::models
src/windows/wslc/services/ContainerModel.h new
+290
@@ -0,0 +1,290 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerModel.h
8 +
9 +Abstract:
10 +
11 + This file contains the ContainerModel definitions
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include <wslservice.h>
18 +#include <wslc.h>
19 +#include <string>
20 +
21 +namespace wsl::windows::wslc::models {
22 +
23 +// Valid formats for container list output.
24 +enum class FormatType
25 +{
26 + Table,
27 + Json,
28 +};
29 +
30 +struct ContainerOptions
31 +{
32 + std::vector<std::string> Arguments;
33 + std::vector<std::string> EnvironmentVariables;
34 + bool Detach = false;
35 + bool Interactive = false;
36 + std::string Name;
37 + bool Remove = false;
38 + bool TTY = false;
39 + bool PublishAll = false;
40 + std::vector<std::string> Ports;
41 + std::vector<std::wstring> Volumes;
42 + std::string WorkingDirectory;
43 + std::vector<std::string> Entrypoint;
44 + std::optional<std::string> User{};
45 + std::optional<std::string> Hostname{};
46 + std::optional<std::string> Domainname{};
47 + std::vector<std::string> DnsServers;
48 + std::vector<std::string> DnsSearchDomains;
49 + std::vector<std::string> DnsOptions;
50 + std::vector<std::string> Tmpfs;
51 + std::vector<std::pair<std::string, std::string>> Labels;
52 +};
53 +
54 +struct CreateContainerResult
55 +{
56 + std::string Id;
57 +};
58 +
59 +struct StopContainerOptions
60 +{
61 + static constexpr LONG DefaultTimeout = -1;
62 +
63 + WSLCSignal Signal = WSLCSignalSIGTERM;
64 + LONG Timeout = DefaultTimeout;
65 +};
66 +
67 +struct KillContainerOptions
68 +{
69 + int Signal = WSLCSignalSIGKILL;
70 +};
71 +
72 +struct PortInformation
73 +{
74 + uint16_t HostPort{};
75 + uint16_t ContainerPort{};
76 + int Protocol{}; // IP protocol number (e.g., IPPROTO_TCP or IPPROTO_UDP)
77 + std::string BindingAddress;
78 +
79 + NLOHMANN_DEFINE_TYPE_INTRUSIVE(PortInformation, HostPort, ContainerPort, Protocol, BindingAddress);
80 +};
81 +
82 +struct ContainerInformation
83 +{
84 + std::string Id;
85 + std::string Name;
86 + std::string Image;
87 + WSLCContainerState State;
88 + ULONGLONG StateChangedAt{};
89 + ULONGLONG CreatedAt{};
90 + std::vector<PortInformation> Ports;
91 +
92 + NLOHMANN_DEFINE_TYPE_INTRUSIVE(ContainerInformation, Id, Name, Image, State, StateChangedAt, CreatedAt, Ports);
93 +};
94 +
95 +struct EnvironmentVariable
96 +{
97 + static std::optional<std::wstring> Parse(const std::wstring& entry);
98 + static std::vector<std::wstring> ParseFile(const std::wstring& filePath);
99 +};
100 +
101 +struct PublishPort
102 +{
103 + enum class Protocol
104 + {
105 + UDP,
106 + TCP,
107 + };
108 +
109 + struct PortRange
110 + {
111 + PortRange(uint16_t start, uint16_t end) : m_start(start), m_end(end)
112 + {
113 + }
114 +
115 + uint16_t Start() const
116 + {
117 + return m_start;
118 + }
119 +
120 + uint16_t End() const
121 + {
122 + return m_end;
123 + }
124 +
125 + constexpr uint16_t Count() const noexcept
126 + {
127 + return (m_end >= m_start) ? (m_end - m_start + 1) : 0;
128 + }
129 +
130 + constexpr bool IsSingle() const noexcept
131 + {
132 + return Count() == 1;
133 + }
134 +
135 + constexpr bool IsValid() const noexcept
136 + {
137 + return Count() > 0 && IsValidPort(m_start) && IsValidPort(m_end);
138 + }
139 +
140 + constexpr bool IsEphemeral() const noexcept
141 + {
142 + return m_start == WSLC_EPHEMERAL_PORT && m_end == WSLC_EPHEMERAL_PORT;
143 + }
144 +
145 + static PublishPort::PortRange ParsePortPart(const std::string& portPart);
146 + static PublishPort::PortRange Ephemeral() noexcept
147 + {
148 + return {WSLC_EPHEMERAL_PORT, WSLC_EPHEMERAL_PORT};
149 + }
150 +
151 + private:
152 + uint16_t m_start{};
153 + uint16_t m_end{};
154 + };
155 +
156 + struct IPAddress
157 + {
158 + explicit IPAddress(std::string ip) : m_ip(std::move(ip))
159 + {
160 + if (!m_ip.empty() && m_ip.front() == '[' && m_ip.back() == ']')
161 + {
162 + m_isIPv6 = true;
163 + m_ip = m_ip.substr(1, m_ip.size() - 2);
164 + }
165 + }
166 +
167 + std::string IP() const
168 + {
169 + return m_ip;
170 + }
171 +
172 + bool IsIPv6() const
173 + {
174 + return m_isIPv6;
175 + }
176 +
177 + private:
178 + bool m_isIPv6 = false;
179 + std::string m_ip{};
180 + };
181 +
182 + static constexpr uint16_t MAX_PORT = std::numeric_limits<uint16_t>::max();
183 + static constexpr uint16_t MIN_PORT = 1;
184 +
185 + std::optional<IPAddress> HostIP() const noexcept
186 + {
187 + return m_hostIP;
188 + }
189 +
190 + PortRange HostPort() const noexcept
191 + {
192 + return m_hostPort;
193 + }
194 +
195 + PortRange ContainerPort() const noexcept
196 + {
197 + return m_containerPort;
198 + }
199 +
200 + Protocol PortProtocol() const noexcept
201 + {
202 + return m_protocol;
203 + }
204 +
205 + std::string Original() const noexcept
206 + {
207 + return m_original;
208 + }
209 +
210 + bool IsRangeMapping() const noexcept
211 + {
212 + return !m_containerPort.IsSingle();
213 + }
214 +
215 + static PublishPort Parse(const std::string& value);
216 +
217 +private:
218 + std::optional<IPAddress> m_hostIP;
219 + PortRange m_hostPort = PortRange::Ephemeral();
220 + PortRange m_containerPort = PortRange::Ephemeral();
221 + Protocol m_protocol = Protocol::TCP;
222 + std::string m_original;
223 + void Validate() const;
224 + PublishPort() = default;
225 + static constexpr bool IsValidPort(unsigned long port) noexcept
226 + {
227 + return port >= MIN_PORT && port <= MAX_PORT;
228 + }
229 +};
230 +
231 +struct VolumeMount
232 +{
233 + std::wstring Host() const
234 + {
235 + return m_host;
236 + }
237 +
238 + std::string ContainerPath() const
239 + {
240 + return m_containerPath;
241 + }
242 +
243 + bool IsReadOnly() const
244 + {
245 + return m_isReadOnlyMode;
246 + }
247 +
248 + bool IsNamedVolume() const
249 + {
250 + return m_isNamedVolume;
251 + }
252 +
253 + static bool IsValidNamedVolumeName(const std::wstring& name);
254 +
255 + static VolumeMount Parse(const std::wstring& value);
256 +
257 +private:
258 + std::wstring m_host;
259 + std::string m_containerPath;
260 + bool m_isReadOnlyMode = false;
261 + bool m_isNamedVolume = false;
262 +
263 + static bool IsReadOnlyMode(const std::wstring& mode)
264 + {
265 + return mode == L"ro";
266 + }
267 +
268 + static bool IsValidMode(const std::wstring& mode)
269 + {
270 + return IsReadOnlyMode(mode) || mode == L"rw";
271 + }
272 +};
273 +
274 +struct TmpfsMount
275 +{
276 + std::string ContainerPath() const
277 + {
278 + return m_containerPath;
279 + }
280 + std::string Options() const
281 + {
282 + return m_options;
283 + }
284 + static TmpfsMount Parse(const std::string& value);
285 +
286 +private:
287 + std::string m_containerPath;
288 + std::string m_options;
289 +};
290 +} // namespace wsl::windows::wslc::models
src/windows/wslc/services/ContainerService.cpp new
+500
@@ -0,0 +1,500 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerService.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the ContainerService implementation
12 +
13 +--*/
14 +
15 +#include <precomp.h>
16 +#include "ContainerService.h"
17 +#include "ConsoleService.h"
18 +#include "ImageService.h"
19 +#include "ImageProgressCallback.h"
20 +#include <wslutil.h>
21 +#include <WSLCProcessLauncher.h>
22 +#include <CommandLine.h>
23 +#include <unordered_map>
24 +#include <wslc.h>
25 +
26 +namespace wsl::windows::wslc::services {
27 +using wsl::windows::common::ClientRunningWSLCProcess;
28 +using wsl::windows::common::wslc_schema::InspectContainer;
29 +using wsl::windows::common::wslutil::PrintMessage;
30 +using namespace wsl::windows::common::wslutil;
31 +using namespace wsl::shared;
32 +using namespace wsl::windows::wslc::models;
33 +using namespace std::chrono_literals;
34 +
35 +static void SetContainerArguments(WSLCProcessOptions& options, std::vector<const char*>& argsStorage)
36 +{
37 + options.CommandLine = {.Values = argsStorage.data(), .Count = static_cast<ULONG>(argsStorage.size())};
38 +}
39 +
40 +static wsl::windows::common::RunningWSLCContainer CreateInternal(Session& session, const std::string& image, const ContainerOptions& options)
41 +{
42 + auto processFlags = WSLCProcessFlagsNone;
43 + WI_SetFlagIf(processFlags, WSLCProcessFlagsStdin, options.Interactive);
44 + WI_SetFlagIf(processFlags, WSLCProcessFlagsTty, options.TTY);
45 +
46 + auto containerFlags = WSLCContainerFlagsNone;
47 + WI_SetFlagIf(containerFlags, WSLCContainerFlagsRm, options.Remove);
48 + WI_SetFlagIf(containerFlags, WSLCContainerFlagsPublishAll, options.PublishAll);
49 +
50 + wsl::windows::common::WSLCContainerLauncher containerLauncher(
51 + image, options.Name, options.Arguments, options.EnvironmentVariables, WSLCContainerNetworkTypeBridged, processFlags);
52 +
53 + // Set port options if provided
54 + for (const auto& port : options.Ports)
55 + {
56 + auto portMapping = PublishPort::Parse(port);
57 +
58 + {
59 + // https://github.com/microsoft/WSL/issues/14433
60 + // The following scenarios are currently not implemented:
61 + // - Host port mappings with a specific host IP
62 + // - Host port mappings with UDP protocol
63 + if (portMapping.HostIP().has_value() || portMapping.PortProtocol() == PublishPort::Protocol::UDP)
64 + {
65 + THROW_HR_WITH_USER_ERROR(
66 + HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
67 + "Port mappings with specific host IPs or UDP protocol are not currently supported");
68 + }
69 + }
70 +
71 + auto containerPort = portMapping.ContainerPort();
72 + for (uint16_t i = 0; i < containerPort.Count(); ++i)
73 + {
74 + auto currentContainerPort = static_cast<uint16_t>(containerPort.Start() + i);
75 + auto currentHostPort = static_cast<uint16_t>(portMapping.HostPort().Start() + i);
76 + containerLauncher.AddPort(currentHostPort, currentContainerPort, AF_INET);
77 + }
78 + }
79 +
80 + // Add volumes if specified
81 + for (const auto& volumeSpec : options.Volumes)
82 + {
83 + auto volume = VolumeMount::Parse(volumeSpec);
84 + auto host = volume.Host();
85 + auto container = volume.ContainerPath();
86 + if (volume.IsNamedVolume())
87 + {
88 + containerLauncher.AddNamedVolume(string::WideToMultiByte(host), container, volume.IsReadOnly());
89 + }
90 + else
91 + {
92 + containerLauncher.AddVolume(host, container, volume.IsReadOnly());
93 + }
94 + }
95 +
96 + containerLauncher.SetContainerFlags(containerFlags);
97 +
98 + if (!options.Entrypoint.empty())
99 + {
100 + auto entrypoints = options.Entrypoint;
101 + containerLauncher.SetEntrypoint(std::move(entrypoints));
102 + }
103 +
104 + if (options.User.has_value())
105 + {
106 + auto user = options.User.value();
107 + containerLauncher.SetUser(std::move(user));
108 + }
109 +
110 + if (!options.WorkingDirectory.empty())
111 + {
112 + containerLauncher.SetWorkingDirectory(std::string(options.WorkingDirectory));
113 + }
114 +
115 + if (options.Hostname.has_value())
116 + {
117 + containerLauncher.SetHostname(std::string(options.Hostname.value()));
118 + }
119 +
120 + if (options.Domainname.has_value())
121 + {
122 + containerLauncher.SetDomainname(std::string(options.Domainname.value()));
123 + }
124 +
125 + if (!options.DnsServers.empty())
126 + {
127 + containerLauncher.SetDnsServers(std::vector<std::string>(options.DnsServers));
128 + }
129 +
130 + if (!options.DnsSearchDomains.empty())
131 + {
132 + containerLauncher.SetDnsSearchDomains(std::vector<std::string>(options.DnsSearchDomains));
133 + }
134 +
135 + if (!options.DnsOptions.empty())
136 + {
137 + containerLauncher.SetDnsOptions(std::vector<std::string>(options.DnsOptions));
138 + }
139 +
140 + for (const auto& tmpfsSpec : options.Tmpfs)
141 + {
142 + auto tmpfsMount = TmpfsMount::Parse(tmpfsSpec);
143 + containerLauncher.AddTmpfs(tmpfsMount.ContainerPath(), tmpfsMount.Options());
144 + }
145 +
146 + for (const auto& [key, value] : options.Labels)
147 + {
148 + containerLauncher.AddLabel(key, value);
149 + }
150 +
151 + auto [result, runningContainer] = containerLauncher.CreateNoThrow(*session.Get());
152 + if (result == WSLC_E_IMAGE_NOT_FOUND)
153 + {
154 + {
155 + // Attempt to pull the image if not found
156 + ImageProgressCallback callback;
157 + PrintMessage(Localization::WSLCCLI_ImageNotFoundPulling(wsl::shared::string::MultiByteToWide(image)), stderr);
158 + ImageService imageService;
159 + imageService.Pull(session, image, &callback);
160 + }
161 + return containerLauncher.Create(*session.Get());
162 + }
163 +
164 + THROW_IF_FAILED(result);
165 + ASSERT(runningContainer);
166 + return std::move(*runningContainer);
167 +}
168 +
169 +static PortInformation PortInformationFromWSLCPortMapping(const WSLCPortMapping& mapping)
170 +{
171 + return PortInformation{
172 + .HostPort = mapping.HostPort,
173 + .ContainerPort = mapping.ContainerPort,
174 + .Protocol = static_cast<int>(mapping.Protocol),
175 + .BindingAddress = mapping.BindingAddress,
176 + };
177 +}
178 +
179 +std::wstring ContainerService::FormatRelativeTime(ULONGLONG timestamp)
180 +{
181 + if (timestamp == 0)
182 + {
183 + return L"";
184 + }
185 +
186 + constexpr LONGLONG SecondsPerMinute = std::chrono::duration_cast<std::chrono::seconds>(1min).count();
187 + constexpr LONGLONG SecondsPerHour = std::chrono::duration_cast<std::chrono::seconds>(1h).count();
188 + constexpr LONGLONG SecondsPerDay = std::chrono::duration_cast<std::chrono::seconds>(24h).count();
189 + constexpr LONGLONG SecondsPerWeek = SecondsPerDay * 7;
190 + constexpr LONGLONG SecondsPerMonth = SecondsPerDay * 30;
191 + constexpr LONGLONG SecondsPerYear = SecondsPerDay * 365;
192 +
193 + auto elapsed = static_cast<LONGLONG>(std::time(nullptr)) - static_cast<LONGLONG>(timestamp);
194 + if (elapsed < 0)
195 + {
196 + elapsed = 0;
197 + }
198 +
199 + auto pluralize = [](LONGLONG count, const wchar_t* singular, const wchar_t* plural) {
200 + return std::format(L"{} {} ago", count, (count == 1 ? singular : plural));
201 + };
202 +
203 + if (elapsed < SecondsPerMinute)
204 + {
205 + return pluralize(elapsed, L"second", L"seconds");
206 + }
207 + else if (elapsed < SecondsPerHour)
208 + {
209 + return pluralize(elapsed / SecondsPerMinute, L"minute", L"minutes");
210 + }
211 + else if (elapsed < SecondsPerDay)
212 + {
213 + return pluralize(elapsed / SecondsPerHour, L"hour", L"hours");
214 + }
215 + else if (elapsed < SecondsPerWeek)
216 + {
217 + return pluralize(elapsed / SecondsPerDay, L"day", L"days");
218 + }
219 + else if (elapsed < SecondsPerMonth)
220 + {
221 + return pluralize(elapsed / SecondsPerWeek, L"week", L"weeks");
222 + }
223 + else if (elapsed < SecondsPerYear)
224 + {
225 + return pluralize(elapsed / SecondsPerMonth, L"month", L"months");
226 + }
227 +
228 + return pluralize(elapsed / SecondsPerYear, L"year", L"years");
229 +}
230 +
231 +int ContainerService::Attach(Session& session, const std::string& id)
232 +{
233 + wil::com_ptr<IWSLCContainer> container;
234 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
235 +
236 + wil::com_ptr<IWSLCProcess> process;
237 + THROW_IF_FAILED(container->GetInitProcess(&process));
238 +
239 + WSLCProcessFlags processFlags{};
240 + THROW_IF_FAILED(process->GetFlags(&processFlags));
241 +
242 + ClientRunningWSLCProcess runningProcess(std::move(process), processFlags);
243 +
244 + COMOutputHandle stdinLogs{};
245 + COMOutputHandle stdoutLogs{};
246 + COMOutputHandle stderrLogs{};
247 + THROW_IF_FAILED(container->Attach(nullptr, &stdinLogs, &stdoutLogs, &stderrLogs));
248 +
249 + if (!stdoutLogs.Empty())
250 + {
251 + // Non-TTY process - relay separate stdout/stderr streams
252 + WI_ASSERT(!stderrLogs.Empty());
253 + ConsoleService::RelayNonTtyProcess(stdinLogs.Release(), stdoutLogs.Release(), stderrLogs.Release());
254 + }
255 + else
256 + {
257 + // TTY process - relay using interactive TTY handling
258 + WI_ASSERT(stderrLogs.Empty());
259 + if (!ConsoleService::RelayInteractiveTty(runningProcess, stdinLogs.Release().get(), true))
260 + {
261 + wsl::windows::common::wslutil::PrintMessage(L"[detached]", stderr);
262 + return 0; // Exit early if user detached
263 + }
264 + }
265 +
266 + // Wait for the container process to exit
267 + return runningProcess.Wait();
268 +}
269 +
270 +std::wstring ContainerService::ContainerStateToString(WSLCContainerState state, ULONGLONG stateChangedAt)
271 +{
272 + std::wstring stateString;
273 + switch (state)
274 + {
275 + case WSLCContainerState::WslcContainerStateCreated:
276 + stateString = L"created";
277 + break;
278 + case WSLCContainerState::WslcContainerStateRunning:
279 + stateString = L"running";
280 + break;
281 + case WSLCContainerState::WslcContainerStateDeleted:
282 + stateString = L"stopped";
283 + break;
284 + case WSLCContainerState::WslcContainerStateExited:
285 + stateString = L"exited";
286 + break;
287 + case WSLCContainerState::WslcContainerStateInvalid:
288 + return L"invalid";
289 + default:
290 + THROW_HR(E_UNEXPECTED);
291 + }
292 +
293 + if (stateChangedAt == 0)
294 + {
295 + return stateString;
296 + }
297 +
298 + return std::format(L"{} {}", stateString, FormatRelativeTime(stateChangedAt));
299 +}
300 +
301 +std::wstring ContainerService::FormatPorts(WSLCContainerState state, const std::vector<PortInformation>& ports)
302 +{
303 + if (state != WslcContainerStateRunning || ports.empty())
304 + {
305 + return L"";
306 + }
307 +
308 + std::wstring result;
309 + for (size_t i = 0; i < ports.size(); ++i)
310 + {
311 + const auto& port = ports[i];
312 +
313 + std::wstring hostIp = wsl::shared::string::MultiByteToWide(port.BindingAddress);
314 +
315 + std::wstring protocol = (port.Protocol == IPPROTO_TCP) ? L"tcp"
316 + : (port.Protocol == IPPROTO_UDP) ? L"udp"
317 + : std::format(L"{}", port.Protocol);
318 +
319 + if (i > 0)
320 + {
321 + result += L", ";
322 + }
323 +
324 + result += std::format(
325 + L"{}:{}->{}/{}", (hostIp.find(L':') != std::wstring::npos) ? std::format(L"[{}]", hostIp) : hostIp, port.HostPort, port.ContainerPort, protocol);
326 + }
327 +
328 + return result;
329 +}
330 +
331 +int ContainerService::Run(Session& session, const std::string& image, ContainerOptions runOptions)
332 +{
333 + // Create the container
334 + auto runningContainer = CreateInternal(session, image, runOptions);
335 + auto& container = runningContainer.Get();
336 +
337 + // Start the created container
338 + WSLCContainerStartFlags startFlags{};
339 + WI_SetFlagIf(startFlags, WSLCContainerStartFlagsAttach, !runOptions.Detach);
340 + THROW_IF_FAILED(container.Start(startFlags, nullptr)); // TODO: Error message, detach keys
341 +
342 + // Disable auto-delete only after successful start
343 + runningContainer.SetDeleteOnClose(false);
344 +
345 + // Handle attach if requested
346 + if (WI_IsFlagSet(startFlags, WSLCContainerStartFlagsAttach))
347 + {
348 + ConsoleService consoleService;
349 + return consoleService.AttachToCurrentConsole(runningContainer.GetInitProcess());
350 + }
351 +
352 + WSLCContainerId containerId{};
353 + THROW_IF_FAILED(container.GetId(containerId));
354 + PrintMessage(L"%hs", stdout, containerId);
355 + return 0;
356 +}
357 +
358 +CreateContainerResult ContainerService::Create(Session& session, const std::string& image, ContainerOptions runOptions)
359 +{
360 + auto runningContainer = CreateInternal(session, image, runOptions);
361 + runningContainer.SetDeleteOnClose(false);
362 + auto& container = runningContainer.Get();
363 + WSLCContainerId id{};
364 + THROW_IF_FAILED(container.GetId(id));
365 + return {.Id = id};
366 +}
367 +
368 +int ContainerService::Start(Session& session, const std::string& id, bool attach)
369 +{
370 + wil::com_ptr<IWSLCContainer> container;
371 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
372 + WSLCContainerStartFlags flags = attach ? WSLCContainerStartFlagsAttach : WSLCContainerStartFlagsNone;
373 + THROW_IF_FAILED_EXCEPT(container->Start(flags, nullptr), WSLC_E_CONTAINER_IS_RUNNING);
374 +
375 + if (!attach)
376 + {
377 + return 0;
378 + }
379 +
380 + wil::com_ptr<IWSLCProcess> process;
381 + THROW_IF_FAILED(container->GetInitProcess(&process));
382 +
383 + WSLCProcessFlags processFlags{};
384 + THROW_IF_FAILED(process->GetFlags(&processFlags));
385 + ClientRunningWSLCProcess runningProcess(std::move(process), processFlags);
386 +
387 + ConsoleService consoleService;
388 + return consoleService.AttachToCurrentConsole(std::move(runningProcess));
389 +}
390 +
391 +void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options)
392 +{
393 + wil::com_ptr<IWSLCContainer> container;
394 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
395 + THROW_IF_FAILED_EXCEPT(container->Stop(options.Signal, options.Timeout), WSLC_E_CONTAINER_NOT_RUNNING);
396 +}
397 +
398 +void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal signal)
399 +{
400 + wil::com_ptr<IWSLCContainer> container;
401 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
402 + THROW_IF_FAILED(container->Kill(signal));
403 +}
404 +
405 +void ContainerService::Delete(Session& session, const std::string& id, bool force)
406 +{
407 + wil::com_ptr<IWSLCContainer> container;
408 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
409 + THROW_IF_FAILED(container->Delete(force ? WSLCDeleteFlagsForce : WSLCDeleteFlagsNone));
410 +}
411 +
412 +std::vector<ContainerInformation> ContainerService::List(Session& session)
413 +{
414 + std::vector<ContainerInformation> result;
415 + wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
416 + wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
417 + THROW_IF_FAILED(session.Get()->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
418 +
419 + for (const auto& current : containers)
420 + {
421 + ContainerInformation entry;
422 + entry.Name = current.Name;
423 + entry.Image = current.Image;
424 + entry.State = current.State;
425 + entry.Id = current.Id;
426 + entry.StateChangedAt = current.StateChangedAt;
427 + entry.CreatedAt = current.CreatedAt;
428 +
429 + for (const auto& port : ports)
430 + {
431 + if (strcmp(port.Id, current.Id) == 0)
432 + {
433 + entry.Ports.push_back(PortInformationFromWSLCPortMapping(port.PortMapping));
434 + }
435 + }
436 +
437 + result.emplace_back(std::move(entry));
438 + }
439 +
440 + return result;
441 +}
442 +
443 +int ContainerService::Exec(Session& session, const std::string& id, ContainerOptions options)
444 +{
445 + wil::com_ptr<IWSLCContainer> container;
446 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
447 +
448 + auto execFlags = WSLCProcessFlagsNone;
449 + WI_SetFlagIf(execFlags, WSLCProcessFlagsStdin, options.Interactive);
450 + WI_SetFlagIf(execFlags, WSLCProcessFlagsTty, options.TTY);
451 +
452 + auto processLauncher = wsl::windows::common::WSLCProcessLauncher({}, options.Arguments, options.EnvironmentVariables, execFlags);
453 + if (options.User.has_value())
454 + {
455 + auto user = options.User.value();
456 + processLauncher.SetUser(std::move(user));
457 + }
458 + if (!options.WorkingDirectory.empty())
459 + {
460 + processLauncher.SetWorkingDirectory(std::move(options.WorkingDirectory));
461 + }
462 +
463 + return ConsoleService::AttachToCurrentConsole(processLauncher.Launch(*container));
464 +}
465 +
466 +InspectContainer ContainerService::Inspect(Session& session, const std::string& id)
467 +{
468 + wil::com_ptr<IWSLCContainer> container;
469 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
470 + wil::unique_cotaskmem_ansistring output;
471 + THROW_IF_FAILED(container->Inspect(&output));
472 + return wsl::shared::FromJson<InspectContainer>(output.get());
473 +}
474 +
475 +void ContainerService::Logs(Session& session, const std::string& id, bool follow)
476 +{
477 + wil::com_ptr<IWSLCContainer> container;
478 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
479 +
480 + COMOutputHandle stdoutHandle;
481 + COMOutputHandle stderrHandle;
482 + WSLCLogsFlags flags = WSLCLogsFlagsNone;
483 + WI_SetFlagIf(flags, WSLCLogsFlagsFollow, follow);
484 +
485 + THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, 0, 0, 0));
486 +
487 + wsl::windows::common::relay::MultiHandleWait io;
488 + io.AddHandle(std::make_unique<wsl::windows::common::relay::RelayHandle<wsl::windows::common::relay::ReadHandle>>(
489 + stdoutHandle.Release(), GetStdHandle(STD_OUTPUT_HANDLE)));
490 +
491 + if (!stderrHandle.Empty()) // This handle is only used for non-tty processes.
492 + {
493 + io.AddHandle(std::make_unique<wsl::windows::common::relay::RelayHandle<wsl::windows::common::relay::ReadHandle>>(
494 + stderrHandle.Release(), GetStdHandle(STD_ERROR_HANDLE)));
495 + }
496 +
497 + // TODO: Handle ctrl-c.
498 + io.Run({});
499 +}
500 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ContainerService.h new
+37
@@ -0,0 +1,37 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerService.h
8 +
9 +Abstract:
10 +
11 + This file contains the ContainerService definition
12 +
13 +--*/
14 +#pragma once
15 +#include "SessionModel.h"
16 +#include "ContainerModel.h"
17 +#include <wslc_schema.h>
18 +
19 +namespace wsl::windows::wslc::services {
20 +struct ContainerService
21 +{
22 + static std::wstring ContainerStateToString(WSLCContainerState state, ULONGLONG stateChangedAt = 0);
23 + static std::wstring FormatRelativeTime(ULONGLONG timestamp);
24 + static std::wstring FormatPorts(WSLCContainerState state, const std::vector<models::PortInformation>& ports);
25 + static int Attach(models::Session& session, const std::string& id);
26 + static int Run(models::Session& session, const std::string& image, models::ContainerOptions options);
27 + static models::CreateContainerResult Create(models::Session& session, const std::string& image, models::ContainerOptions options);
28 + static int Start(models::Session& session, const std::string& id, bool attach = false);
29 + static void Stop(models::Session& session, const std::string& id, models::StopContainerOptions options);
30 + static void Kill(models::Session& session, const std::string& id, WSLCSignal signal = WSLCSignalSIGKILL);
31 + static void Delete(models::Session& session, const std::string& id, bool force);
32 + static std::vector<models::ContainerInformation> List(models::Session& session);
33 + static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options);
34 + static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
35 + static void Logs(models::Session& session, const std::string& id, bool follow);
36 +};
37 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/FileCredStorage.cpp new
+255
@@ -0,0 +1,255 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + FileCredStorage.cpp
8 +
9 +Abstract:
10 +
11 + DPAPI-encrypted JSON file credential storage implementation.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "FileCredStorage.h"
17 +
18 +using wsl::shared::Localization;
19 +
20 +using namespace wsl::shared;
21 +using namespace wsl::windows::common::wslutil;
22 +using namespace wsl::windows::wslc::services;
23 +
24 +namespace {
25 +
26 +std::filesystem::path GetFilePath()
27 +{
28 + return wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"registry-credentials.json";
29 +}
30 +
31 +wil::unique_file RetryOpenFileOnSharingViolation(const std::function<wil::unique_file()>& openFunc)
32 +{
33 + try
34 + {
35 + return wsl::shared::retry::RetryWithTimeout<wil::unique_file>(openFunc, std::chrono::milliseconds(100), std::chrono::seconds(1), []() {
36 + return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION);
37 + });
38 + }
39 + catch (...)
40 + {
41 + auto result = wil::ResultFromCaughtException();
42 + auto errorString = wsl::windows::common::wslutil::GetSystemErrorString(result);
43 + THROW_HR_WITH_USER_ERROR(result, Localization::MessageWslcFailedToOpenFile(GetFilePath(), errorString));
44 + }
45 +}
46 +
47 +wil::unique_file OpenFileExclusive()
48 +{
49 + wil::unique_file f(_wfsopen(GetFilePath().c_str(), L"r+b", _SH_DENYRW));
50 + if (!f)
51 + {
52 + auto dosError = _doserrno;
53 + if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND)
54 + {
55 + return nullptr;
56 + }
57 +
58 + THROW_WIN32_IF(dosError, dosError != 0);
59 + THROW_HR(E_FAIL);
60 + }
61 +
62 + return f;
63 +}
64 +
65 +wil::unique_file CreateFileExclusive()
66 +{
67 + auto filePath = GetFilePath();
68 + std::filesystem::create_directories(filePath.parent_path());
69 +
70 + using UniqueFd = wil::unique_any<int, decltype(_close), _close, wil::details::pointer_access_all, int, int, -1>;
71 +
72 + UniqueFd fd;
73 + auto err = _wsopen_s(fd.addressof(), filePath.c_str(), _O_RDWR | _O_CREAT | _O_BINARY, _SH_DENYRW, _S_IREAD | _S_IWRITE);
74 + if (err != 0)
75 + {
76 + auto dosError = _doserrno;
77 + THROW_WIN32_IF(dosError, dosError != 0);
78 + THROW_HR(E_FAIL);
79 + }
80 +
81 + wil::unique_file f(_fdopen(fd.get(), "r+b"));
82 + if (!f)
83 + {
84 + auto dosError = _doserrno;
85 + THROW_WIN32_IF(dosError, dosError != 0);
86 + THROW_HR(E_FAIL);
87 + }
88 +
89 + fd.release();
90 + return f;
91 +}
92 +
93 +wil::unique_file OpenFileShared()
94 +{
95 + wil::unique_file f(_wfsopen(GetFilePath().c_str(), L"rb", _SH_DENYWR));
96 + if (!f)
97 + {
98 + auto dosError = _doserrno;
99 + if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND)
100 + {
101 + return nullptr;
102 + }
103 +
104 + THROW_WIN32_IF(dosError, dosError != 0);
105 + THROW_HR(E_FAIL);
106 + }
107 +
108 + return f;
109 +}
110 +
111 +CredentialFile ReadCredentialFile(FILE* f)
112 +{
113 + WI_ASSERT(f != nullptr);
114 +
115 + auto seekResult = fseek(f, 0, SEEK_SET);
116 + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), seekResult != 0);
117 +
118 + // Handle newly created empty files (from CreateFileExclusive).
119 + if (_filelengthi64(_fileno(f)) <= 0)
120 + {
121 + return {};
122 + }
123 +
124 + try
125 + {
126 + return nlohmann::json::parse(f).get<CredentialFile>();
127 + }
128 + catch (const nlohmann::json::exception&)
129 + {
130 + THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_JSON, Localization::WSLCCLI_CredentialFileCorrupt(GetFilePath()));
131 + }
132 +}
133 +
134 +void WriteCredentialFile(FILE* f, const CredentialFile& data)
135 +{
136 + auto error = fseek(f, 0, SEEK_SET);
137 + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(errno)), error != 0);
138 +
139 + error = _chsize_s(_fileno(f), 0);
140 + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(error)), error != 0);
141 +
142 + auto content = nlohmann::json(data).dump(2);
143 + auto written = fwrite(content.data(), 1, content.size(), f);
144 + THROW_HR_WITH_USER_ERROR_IF(
145 + E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(errno)), written != content.size());
146 +}
147 +
148 +void ModifyFileStore(FILE* f, const std::function<bool(CredentialFile&)>& modifier)
149 +{
150 + auto data = ReadCredentialFile(f);
151 +
152 + if (modifier(data))
153 + {
154 + WriteCredentialFile(f, data);
155 + }
156 +}
157 +
158 +std::string Protect(const std::string& plaintext)
159 +{
160 + DATA_BLOB input{};
161 + input.cbData = static_cast<DWORD>(plaintext.size());
162 + input.pbData = reinterpret_cast<BYTE*>(const_cast<char*>(plaintext.data()));
163 +
164 + DATA_BLOB output{};
165 + THROW_IF_WIN32_BOOL_FALSE(CryptProtectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output));
166 + auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); });
167 +
168 + return Base64Encode(std::string(reinterpret_cast<const char*>(output.pbData), output.cbData));
169 +}
170 +
171 +std::string Unprotect(const std::string& cipherBase64)
172 +{
173 + auto decoded = Base64Decode(cipherBase64);
174 +
175 + DATA_BLOB input{};
176 + input.cbData = static_cast<DWORD>(decoded.size());
177 + input.pbData = reinterpret_cast<BYTE*>(decoded.data());
178 +
179 + DATA_BLOB output{};
180 + THROW_IF_WIN32_BOOL_FALSE(CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output));
181 + auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); });
182 +
183 + return std::string(reinterpret_cast<const char*>(output.pbData), output.cbData);
184 +}
185 +
186 +} // namespace
187 +
188 +namespace wsl::windows::wslc::services {
189 +
190 +void FileCredStorage::Store(const std::string& serverAddress, const std::string& username, const std::string& secret)
191 +{
192 + auto file = RetryOpenFileOnSharingViolation(CreateFileExclusive);
193 +
194 + ModifyFileStore(file.get(), [&](CredentialFile& data) {
195 + data.Credentials[serverAddress] = CredentialEntry{username, Protect(secret)};
196 + return true;
197 + });
198 +}
199 +
200 +std::pair<std::string, std::string> FileCredStorage::Get(const std::string& serverAddress)
201 +{
202 + auto file = RetryOpenFileOnSharingViolation(OpenFileShared);
203 + if (!file)
204 + {
205 + return {};
206 + }
207 +
208 + auto data = ReadCredentialFile(file.get());
209 + const auto entry = data.Credentials.find(serverAddress);
210 +
211 + if (entry == data.Credentials.end())
212 + {
213 + return {};
214 + }
215 +
216 + return {entry->second.UserName, Unprotect(entry->second.Secret)};
217 +}
218 +
219 +void FileCredStorage::Erase(const std::string& serverAddress)
220 +{
221 + auto file = RetryOpenFileOnSharingViolation(OpenFileExclusive);
222 + bool erased = false;
223 +
224 + if (file)
225 + {
226 + ModifyFileStore(file.get(), [&](CredentialFile& data) {
227 + erased = data.Credentials.erase(serverAddress) > 0;
228 + return erased;
229 + });
230 + }
231 +
232 + THROW_HR_WITH_USER_ERROR_IF(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), !erased);
233 +}
234 +
235 +std::vector<std::wstring> FileCredStorage::List()
236 +{
237 + auto file = RetryOpenFileOnSharingViolation(OpenFileShared);
238 + if (!file)
239 + {
240 + return {};
241 + }
242 +
243 + auto data = ReadCredentialFile(file.get());
244 +
245 + std::vector<std::wstring> result;
246 +
247 + for (const auto& [key, value] : data.Credentials)
248 + {
249 + result.push_back(wsl::shared::string::MultiByteToWide(key));
250 + }
251 +
252 + return result;
253 +}
254 +
255 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/FileCredStorage.h new
+47
@@ -0,0 +1,47 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + FileCredStorage.h
8 +
9 +Abstract:
10 +
11 + DPAPI-encrypted JSON file credential storage backend.
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include "ICredentialStorage.h"
17 +#include "JsonUtils.h"
18 +
19 +namespace wsl::windows::wslc::services {
20 +
21 +inline constexpr int CredentialFileVersion = 1;
22 +
23 +struct CredentialEntry
24 +{
25 + std::string UserName;
26 + std::string Secret;
27 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CredentialEntry, UserName, Secret);
28 +};
29 +
30 +struct CredentialFile
31 +{
32 + int Version = CredentialFileVersion;
33 + std::map<std::string, CredentialEntry> Credentials;
34 +
35 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CredentialFile, Version, Credentials);
36 +};
37 +
38 +class FileCredStorage final : public ICredentialStorage
39 +{
40 +public:
41 + void Store(const std::string& serverAddress, const std::string& username, const std::string& secret) override;
42 + std::pair<std::string, std::string> Get(const std::string& serverAddress) override;
43 + void Erase(const std::string& serverAddress) override;
44 + std::vector<std::wstring> List() override;
45 +};
46 +
47 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ICredentialStorage.cpp new
+33
@@ -0,0 +1,33 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ICredentialStorage.cpp
8 +
9 +Abstract:
10 +
11 + Factory for credential storage backends.
12 +
13 +--*/
14 +
15 +#include "ICredentialStorage.h"
16 +#include "FileCredStorage.h"
17 +#include "WinCredStorage.h"
18 +#include "WSLCUserSettings.h"
19 +
20 +namespace wsl::windows::wslc::services {
21 +
22 +std::unique_ptr<ICredentialStorage> OpenCredentialStorage()
23 +{
24 + auto backend = settings::User().Get<settings::Setting::CredentialStore>();
25 + if (backend == settings::CredentialStoreType::File)
26 + {
27 + return std::make_unique<FileCredStorage>();
28 + }
29 +
30 + return std::make_unique<WinCredStorage>();
31 +}
32 +
33 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ICredentialStorage.h new
+36
@@ -0,0 +1,36 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ICredentialStorage.h
8 +
9 +Abstract:
10 +
11 + Interface for credential storage backends.
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include <memory>
17 +#include <string>
18 +#include <vector>
19 +
20 +namespace wsl::windows::wslc::services {
21 +
22 +// Abstract interface for credential storage backends (WinCred, file-based, etc.).
23 +struct ICredentialStorage
24 +{
25 + virtual ~ICredentialStorage() = default;
26 +
27 + virtual void Store(const std::string& serverAddress, const std::string& username, const std::string& secret) = 0;
28 + virtual std::pair<std::string, std::string> Get(const std::string& serverAddress) = 0;
29 + virtual void Erase(const std::string& serverAddress) = 0;
30 + virtual std::vector<std::wstring> List() = 0;
31 +};
32 +
33 +// Returns the credential storage implementation based on user configuration.
34 +std::unique_ptr<ICredentialStorage> OpenCredentialStorage();
35 +
36 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ImageModel.h new
+37
@@ -0,0 +1,37 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageModel.h
8 +
9 +Abstract:
10 +
11 + This file contains the ImageModel definition.
12 +
13 +--*/
14 +#pragma once
15 +
16 +// 1000*1000 instead of 1024*1024 to be consistent with Docker CLI's definition of megabyte (MB).
17 +#define WSLC_IMAGE_1MB (1000 * 1000)
18 +
19 +namespace wsl::windows::wslc::models {
20 +struct ImageInformation
21 +{
22 + std::optional<std::string> Repository;
23 + std::optional<std::string> Tag;
24 + std::string Id;
25 + LONGLONG Created{};
26 + ULONGLONG Size{};
27 +
28 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ImageInformation, Repository, Tag, Id, Created, Size);
29 +};
30 +
31 +struct PruneImagesResult
32 +{
33 + std::vector<std::string> DeletedImages;
34 + std::vector<std::string> UntaggedImages;
35 + ULONGLONG SpaceReclaimed{};
36 +};
37 +} // namespace wsl::windows::wslc::models
src/windows/wslc/services/ImageProgressCallback.cpp new
+128
@@ -0,0 +1,128 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageProgressCallback.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the ImageProgressCallback Implementation.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "ImageProgressCallback.h"
17 +#include "ImageService.h"
18 +#include <format>
19 +
20 +namespace wsl::windows::wslc::services {
21 +using namespace wsl::shared;
22 +
23 +ChangeTerminalMode::ChangeTerminalMode(HANDLE console, bool cursorVisible) : m_console(console)
24 +{
25 + if (!wsl::windows::common::wslutil::IsConsoleHandle(console))
26 + {
27 + m_console = nullptr;
28 + return;
29 + }
30 +
31 + THROW_IF_WIN32_BOOL_FALSE(GetConsoleCursorInfo(console, &m_originalCursorInfo));
32 + CONSOLE_CURSOR_INFO newCursorInfo = m_originalCursorInfo;
33 + newCursorInfo.bVisible = cursorVisible;
34 + THROW_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(console, &newCursorInfo));
35 +}
36 +
37 +ChangeTerminalMode::~ChangeTerminalMode()
38 +{
39 + if (m_console)
40 + {
41 + LOG_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(m_console, &m_originalCursorInfo));
42 + }
43 +}
44 +
45 +auto ImageProgressCallback::MoveToLine(SHORT line)
46 +{
47 + if (line > 0)
48 + {
49 + wprintf(L"\033[%iA", line);
50 + }
51 +
52 + return wil::scope_exit([line = line]() {
53 + if (line > 1)
54 + {
55 + wprintf(L"\033[%iB", line - 1);
56 + }
57 + });
58 +}
59 +
60 +HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total)
61 +{
62 + try
63 + {
64 + if (!m_terminalMode.IsConsole())
65 + {
66 + return S_OK;
67 + }
68 +
69 + if (id == nullptr || *id == '\0') // Print all 'global' statuses on their own line
70 + {
71 + wprintf(L"%hs\n", status);
72 + m_currentLine++;
73 + return S_OK;
74 + }
75 +
76 + auto info = Info();
77 +
78 + auto it = m_statuses.find(id);
79 + if (it == m_statuses.end())
80 + {
81 + // If this is the first time we see this ID, create a new line for it.
82 + m_statuses.emplace(id, m_currentLine);
83 + wprintf(L"%ls\n", GenerateStatusLine(status, id, current, total, info).c_str());
84 + m_currentLine++;
85 + }
86 + else
87 + {
88 + auto revert = MoveToLine(m_currentLine - it->second);
89 + wprintf(L"%ls\n", GenerateStatusLine(status, id, current, total, info).c_str());
90 + }
91 +
92 + return S_OK;
93 + }
94 + CATCH_RETURN();
95 +}
96 +
97 +CONSOLE_SCREEN_BUFFER_INFO ImageProgressCallback::Info()
98 +{
99 + CONSOLE_SCREEN_BUFFER_INFO info{};
100 + THROW_IF_WIN32_BOOL_FALSE(GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info));
101 + return info;
102 +}
103 +
104 +std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info)
105 +{
106 + std::wstring line;
107 + if (total != 0)
108 + {
109 + line = std::format(L"{} '{}': {}%", status, id, current * 100 / total);
110 + }
111 + else if (current != 0)
112 + {
113 + line = std::format(L"{} '{}': {}s", status, id, current);
114 + }
115 + else
116 + {
117 + line = std::format(L"{} '{}'", status, id);
118 + }
119 +
120 + // Erase any previously written char on that line.
121 + while (line.size() < info.dwSize.X)
122 + {
123 + line += L' ';
124 + }
125 +
126 + return line;
127 +}
128 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ImageProgressCallback.h new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageProgressCallback.h
8 +
9 +Abstract:
10 +
11 + This file contains the ImageProgressCallback definition
12 +
13 +--*/
14 +#pragma once
15 +#include "SessionService.h"
16 +
17 +namespace wsl::windows::wslc::services {
18 +
19 +class ChangeTerminalMode
20 +{
21 +public:
22 + NON_COPYABLE(ChangeTerminalMode);
23 + NON_MOVABLE(ChangeTerminalMode);
24 + ChangeTerminalMode(HANDLE console, bool cursorVisible);
25 + ~ChangeTerminalMode();
26 +
27 + bool IsConsole() const
28 + {
29 + return m_console != nullptr;
30 + }
31 +
32 +private:
33 + HANDLE m_console{};
34 + CONSOLE_CURSOR_INFO m_originalCursorInfo{};
35 +};
36 +
37 +// TODO: Handle terminal resizes.
38 +class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallback
39 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback, IFastRundown>
40 +{
41 +public:
42 + auto MoveToLine(SHORT line);
43 + HRESULT OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) override;
44 +
45 +private:
46 + static CONSOLE_SCREEN_BUFFER_INFO Info();
47 + std::wstring GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info);
48 + std::map<std::string, SHORT> m_statuses;
49 + SHORT m_currentLine = 0;
50 + ChangeTerminalMode m_terminalMode{GetStdHandle(STD_OUTPUT_HANDLE), false};
51 +};
52 +} // namespace wsl::windows::wslc::services
\ No newline at end of file
src/windows/wslc/services/ImageService.cpp new
+291
@@ -0,0 +1,291 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageService.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the ImageService implementation
12 +
13 +--*/
14 +#include "ImageService.h"
15 +#include "RegistryService.h"
16 +#include "SessionService.h"
17 +#include <wslutil.h>
18 +#include <HandleConsoleProgressBar.h>
19 +
20 +using namespace wsl::shared;
21 +using namespace wsl::windows::common::wslutil;
22 +
23 +namespace {
24 +
25 +wil::unique_hfile ResolveBuildFile(const std::filesystem::path& contextPath)
26 +{
27 + auto containerfilePath = contextPath / L"Containerfile";
28 + auto containerfileStatus = wil::try_open_file(containerfilePath.c_str());
29 +
30 + auto dockerfilePath = contextPath / L"Dockerfile";
31 + auto dockerfileStatus = wil::try_open_file(dockerfilePath.c_str());
32 +
33 + // Fail if both Containerfile and Dockerfile exist.
34 + // Assume that both exist if one opens successfully and the other returns anything other than ERROR_FILE_NOT_FOUND to cover the case where one of them exists, but fails to open.
35 + // If both exist but fail to open, the logic after this block will report the appropriate error.
36 + if ((containerfileStatus.last_error != ERROR_FILE_NOT_FOUND && dockerfileStatus.file) ||
37 + (dockerfileStatus.last_error != ERROR_FILE_NOT_FOUND && containerfileStatus.file))
38 + {
39 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcBothDockerAndContainerFileFound());
40 + }
41 +
42 + if (containerfileStatus.last_error != ERROR_FILE_NOT_FOUND)
43 + {
44 + THROW_HR_WITH_USER_ERROR_IF(
45 + HRESULT_FROM_WIN32(containerfileStatus.last_error),
46 + Localization::MessageWslcFailedToOpenFile(
47 + containerfilePath, wsl::windows::common::wslutil::GetSystemErrorString(HRESULT_FROM_WIN32(containerfileStatus.last_error))),
48 + !containerfileStatus.file.is_valid());
49 +
50 + return std::move(containerfileStatus.file);
51 + }
52 +
53 + if (dockerfileStatus.last_error != ERROR_FILE_NOT_FOUND)
54 + {
55 + THROW_HR_WITH_USER_ERROR_IF(
56 + HRESULT_FROM_WIN32(dockerfileStatus.last_error),
57 + Localization::MessageWslcFailedToOpenFile(
58 + dockerfilePath, wsl::windows::common::wslutil::GetSystemErrorString(HRESULT_FROM_WIN32(dockerfileStatus.last_error))),
59 + !dockerfileStatus.file.is_valid());
60 +
61 + return std::move(dockerfileStatus.file);
62 + }
63 +
64 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcBuildFileNotFound(contextPath));
65 +}
66 +
67 +std::string GetServerFromImage(const std::string& image)
68 +{
69 + auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(image);
70 + auto [server, path] = wsl::windows::common::wslutil::NormalizeRepo(repo);
71 + return server;
72 +}
73 +
74 +} // namespace
75 +
76 +namespace wsl::windows::wslc::services {
77 +
78 +using namespace wsl::windows::wslc::models;
79 +using wsl::windows::common::wslc_schema::InspectImage;
80 +
81 +void ImageService::Build(
82 + wsl::windows::wslc::models::Session& session,
83 + const std::wstring& contextPath,
84 + const std::vector<std::wstring>& tags,
85 + const std::vector<std::wstring>& buildArgs,
86 + const std::wstring& dockerfilePath,
87 + const std::wstring& target,
88 + WSLCBuildImageFlags flags,
89 + IProgressCallback* callback,
90 + HANDLE cancelEvent)
91 +{
92 + auto absolutePath = std::filesystem::absolute(contextPath);
93 + THROW_HR_IF_MSG(
94 + HRESULT_FROM_WIN32(ERROR_DIRECTORY),
95 + !std::filesystem::is_directory(absolutePath),
96 + "Path must be a directory: %ls",
97 + absolutePath.c_str());
98 +
99 + HANDLE dockerfileHandle = nullptr;
100 + wil::unique_hfile dockerfile;
101 + if (dockerfilePath == L"-")
102 + {
103 + dockerfileHandle = GetStdHandle(STD_INPUT_HANDLE);
104 + }
105 + else if (!dockerfilePath.empty())
106 + {
107 + dockerfile.reset(CreateFileW(dockerfilePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
108 + THROW_LAST_ERROR_IF_MSG(!dockerfile, "Failed to open Dockerfile: %ls", dockerfilePath.c_str());
109 + dockerfileHandle = dockerfile.get();
110 + }
111 + else
112 + {
113 + dockerfile = ResolveBuildFile(absolutePath);
114 + dockerfileHandle = dockerfile.get();
115 + }
116 +
117 + auto toMultiByte = [](const std::vector<std::wstring>& input, std::vector<std::string>& strings, std::vector<LPCSTR>& pointers) {
118 + strings.reserve(input.size());
119 + for (const auto& s : input)
120 + {
121 + strings.push_back(wsl::windows::common::string::WideToMultiByte(s));
122 + pointers.push_back(strings.back().c_str());
123 + }
124 + };
125 +
126 + std::vector<std::string> tagStrings;
127 + std::vector<LPCSTR> tagPointers;
128 + toMultiByte(tags, tagStrings, tagPointers);
129 +
130 + std::vector<std::string> buildArgStrings;
131 + std::vector<LPCSTR> buildArgPointers;
132 + toMultiByte(buildArgs, buildArgStrings, buildArgPointers);
133 +
134 + auto targetStr = wsl::windows::common::string::WideToMultiByte(target);
135 +
136 + auto contextPathStr = absolutePath.wstring();
137 + WSLCBuildImageOptions options{
138 + .ContextPath = contextPathStr.c_str(),
139 + .DockerfileHandle = ToCOMInputHandle(dockerfileHandle),
140 + .Tags = {tagPointers.data(), static_cast<ULONG>(tagPointers.size())},
141 + .BuildArgs = {buildArgPointers.data(), static_cast<ULONG>(buildArgPointers.size())},
142 + .Target = targetStr.empty() ? nullptr : targetStr.c_str(),
143 + .Flags = flags,
144 + };
145 +
146 + THROW_IF_FAILED(session.Get()->BuildImage(&options, callback, cancelEvent));
147 +}
148 +
149 +std::vector<ImageInformation> ImageService::List(wsl::windows::wslc::models::Session& session)
150 +{
151 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
152 + ULONG count = 0;
153 + THROW_IF_FAILED(session.Get()->ListImages(nullptr, &images, &count));
154 +
155 + std::vector<ImageInformation> result;
156 + for (auto ptr = images.get(), end = images.get() + count; ptr != end; ++ptr)
157 + {
158 + const WSLCImageInformation& image = *ptr;
159 + ImageInformation info{};
160 +
161 + // Parse the image reference — dangling images have no repo/tag
162 + std::string imageRef = image.Image;
163 + if (imageRef != "<none>:<none>")
164 + {
165 + auto parsed = wsl::windows::common::wslutil::ParseImage(imageRef);
166 + info.Repository = parsed.first;
167 + info.Tag = parsed.second;
168 + }
169 +
170 + info.Id = image.Hash;
171 + info.Created = image.Created;
172 + info.Size = image.Size;
173 + result.push_back(info);
174 + }
175 +
176 + return result;
177 +}
178 +
179 +void ImageService::Load(wsl::windows::wslc::models::Session& session, const std::wstring& input)
180 +{
181 + wil::unique_hfile imageFile{CreateFileW(input.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
182 + THROW_LAST_ERROR_IF(!imageFile);
183 +
184 + LARGE_INTEGER fileSize{};
185 + THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize));
186 +
187 + THROW_IF_FAILED(session.Get()->LoadImage(ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart));
188 +}
189 +
190 +void ImageService::Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune)
191 +{
192 + WSLCDeleteImageOptions options{};
193 + options.Image = image.c_str();
194 +
195 + if (force)
196 + {
197 + options.Flags |= WSLCDeleteImageFlagsForce;
198 + }
199 +
200 + if (noPrune)
201 + {
202 + options.Flags |= WSLCDeleteImageFlagsNoPrune;
203 + }
204 +
205 + wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
206 + THROW_IF_FAILED(session.Get()->DeleteImage(&options, &deletedImages, deletedImages.size_address<ULONG>()));
207 +}
208 +
209 +void ImageService::Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback)
210 +{
211 + auto server = GetServerFromImage(image);
212 + auto auth = RegistryService::Get(server);
213 + THROW_IF_FAILED(session.Get()->PullImage(image.c_str(), auth.c_str(), callback));
214 +}
215 +
216 +void ImageService::Tag(wsl::windows::wslc::models::Session& session, const std::string& sourceImage, const std::string& targetImage)
217 +{
218 + EnumReferenceFormat format;
219 + auto [repo, tag] = ParseImage(targetImage, &format);
220 + if (format == EnumReferenceFormat::Digest)
221 + {
222 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcTagImageInvalidFormat(targetImage.c_str()));
223 + }
224 +
225 + WSLCTagImageOptions options{};
226 + options.Image = sourceImage.c_str();
227 + options.Repo = repo.c_str();
228 + options.Tag = tag ? tag->c_str() : "";
229 +
230 + THROW_IF_FAILED(session.Get()->TagImage(&options));
231 +}
232 +
233 +InspectImage ImageService::Inspect(wsl::windows::wslc::models::Session& session, const std::string& image)
234 +{
235 + wil::unique_cotaskmem_ansistring inspectData;
236 + THROW_IF_FAILED(session.Get()->InspectImage(image.c_str(), &inspectData));
237 + return wsl::shared::FromJson<InspectImage>(inspectData.get());
238 +}
239 +
240 +void ImageService::Push(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback)
241 +{
242 + auto server = GetServerFromImage(image);
243 + auto auth = RegistryService::Get(server);
244 + THROW_IF_FAILED(session.Get()->PushImage(image.c_str(), auth.c_str(), callback));
245 +}
246 +
247 +void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::string& image, const std::wstring& output, HANDLE cancelEvent)
248 +{
249 + wil::unique_hfile outputFile{
250 + CreateFileW(output.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
251 + THROW_LAST_ERROR_IF(!outputFile);
252 +
253 + Save(session, image, outputFile.get(), cancelEvent);
254 +}
255 +
256 +void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::string& image, HANDLE outputHandle, HANDLE cancelEvent)
257 +{
258 + wsl::windows::common::HandleConsoleProgressBar progressBar(
259 + outputHandle, L"Save in progress.", wsl::windows::common::HandleConsoleProgressBar::Format::FileSize);
260 + THROW_IF_FAILED(session.Get()->SaveImage(ToCOMInputHandle(outputHandle), image.c_str(), nullptr, cancelEvent));
261 +}
262 +
263 +wsl::windows::wslc::models::PruneImagesResult ImageService::Prune(wsl::windows::wslc::models::Session& session, bool all)
264 +{
265 + WSLCPruneImagesOptions options{};
266 + if (all)
267 + {
268 + WI_SetFlag(options.Flags, WSLCPruneImagesFlagsDanglingFalse);
269 + }
270 +
271 + wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
272 + ULONGLONG spaceReclaimed = 0;
273 + THROW_IF_FAILED(session.Get()->PruneImages(&options, &deletedImages, deletedImages.size_address<ULONG>(), &spaceReclaimed));
274 +
275 + wsl::windows::wslc::models::PruneImagesResult result;
276 + result.SpaceReclaimed = spaceReclaimed;
277 + for (auto ptr = deletedImages.get(), end = deletedImages.get() + deletedImages.size(); ptr != end; ++ptr)
278 + {
279 + if (ptr->Type == WSLCDeletedImageTypeDeleted)
280 + {
281 + result.DeletedImages.push_back(ptr->Image);
282 + }
283 + else
284 + {
285 + result.UntaggedImages.push_back(ptr->Image);
286 + }
287 + }
288 +
289 + return result;
290 +}
291 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ImageService.h new
+46
@@ -0,0 +1,46 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageService.h
8 +
9 +Abstract:
10 +
11 + This file contains the ImageService definition
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include "SessionModel.h"
17 +#include "ImageModel.h"
18 +#include <wslc_schema.h>
19 +
20 +namespace wsl::windows::wslc::services {
21 +class ImageService
22 +{
23 +public:
24 + static void Build(
25 + wsl::windows::wslc::models::Session& session,
26 + const std::wstring& contextPath,
27 + const std::vector<std::wstring>& tags,
28 + const std::vector<std::wstring>& buildArgs,
29 + const std::wstring& dockerfilePath,
30 + const std::wstring& target,
31 + WSLCBuildImageFlags flags,
32 + IProgressCallback* callback,
33 + HANDLE cancelEvent = nullptr);
34 +
35 + static std::vector<wsl::windows::wslc::models::ImageInformation> List(wsl::windows::wslc::models::Session& session);
36 + static void Load(wsl::windows::wslc::models::Session& session, const std::wstring& input);
37 + static void Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune);
38 + static wsl::windows::common::wslc_schema::InspectImage Inspect(wsl::windows::wslc::models::Session& session, const std::string& image);
39 + static void Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback);
40 + static void Push(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback);
41 + static void Save(wsl::windows::wslc::models::Session& session, const std::string& image, const std::wstring& output, HANDLE cancelEvent = nullptr);
42 + static void Save(wsl::windows::wslc::models::Session& session, const std::string& image, HANDLE outputHandle, HANDLE cancelEvent = nullptr);
43 + static void Tag(wsl::windows::wslc::models::Session& session, const std::string& sourceImage, const std::string& targetImage);
44 + static wsl::windows::wslc::models::PruneImagesResult Prune(wsl::windows::wslc::models::Session& session, bool all);
45 +};
46 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/InspectModel.h new
+25
@@ -0,0 +1,25 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + InspectModel.h
8 +
9 +Abstract:
10 +
11 + This file contains the InspectModel definition
12 +
13 +--*/
14 +#pragma once
15 +
16 +namespace wsl::windows::wslc::models {
17 +typedef enum _InspectType
18 +{
19 + Container = 1,
20 + Image = 2,
21 + Volume = 4,
22 +
23 + All = Container | Image | Volume,
24 +} InspectType;
25 +} // namespace wsl::windows::wslc::models
src/windows/wslc/services/RegistryService.cpp new
+105
@@ -0,0 +1,105 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RegistryService.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the RegistryService implementation
12 +
13 +--*/
14 +
15 +#include "RegistryService.h"
16 +#include <wslutil.h>
17 +
18 +using namespace wsl::windows::common::wslutil;
19 +
20 +namespace {
21 +
22 +std::string ResolveCredentialKey(const std::string& serverAddress)
23 +{
24 + auto input = serverAddress;
25 +
26 + // Strip scheme
27 + if (auto pos = input.find("://"); pos != std::string::npos)
28 + {
29 + input = input.substr(pos + 3);
30 + }
31 +
32 + // Strip path
33 + if (auto pos = input.find('/'); pos != std::string::npos)
34 + {
35 + input = input.substr(0, pos);
36 + }
37 +
38 + // Map Docker Hub aliases to canonical key.
39 + if (input == "docker.io" || input == "index.docker.io")
40 + {
41 + return wsl::windows::wslc::services::RegistryService::DefaultServer;
42 + }
43 +
44 + return input;
45 +}
46 +} // namespace
47 +
48 +namespace wsl::windows::wslc::services {
49 +
50 +// Sentinel username matching Docker's convention for identity-token credentials.
51 +static constexpr auto TokenUsername = "<token>";
52 +
53 +void RegistryService::Store(const std::string& serverAddress, const std::string& username, const std::string& secret)
54 +{
55 + THROW_HR_IF(E_INVALIDARG, serverAddress.empty());
56 + THROW_HR_IF(E_INVALIDARG, secret.empty());
57 +
58 + auto storage = OpenCredentialStorage();
59 + storage->Store(ResolveCredentialKey(serverAddress), username, secret);
60 +}
61 +
62 +std::string RegistryService::Get(const std::string& serverAddress)
63 +{
64 + auto storage = OpenCredentialStorage();
65 + auto key = ResolveCredentialKey(serverAddress);
66 + auto [username, secret] = storage->Get(key);
67 +
68 + if (username == TokenUsername)
69 + {
70 + return BuildRegistryAuthHeader(secret);
71 + }
72 +
73 + return BuildRegistryAuthHeader(username, secret);
74 +}
75 +
76 +void RegistryService::Erase(const std::string& serverAddress)
77 +{
78 + THROW_HR_IF(E_INVALIDARG, serverAddress.empty());
79 +
80 + auto storage = OpenCredentialStorage();
81 + storage->Erase(ResolveCredentialKey(serverAddress));
82 +}
83 +
84 +std::vector<std::wstring> RegistryService::List()
85 +{
86 + auto storage = OpenCredentialStorage();
87 + return storage->List();
88 +}
89 +
90 +std::pair<std::string, std::string> RegistryService::Authenticate(
91 + wsl::windows::wslc::models::Session& session, const std::string& serverAddress, const std::string& username, const std::string& password)
92 +{
93 + wil::unique_cotaskmem_ansistring identityToken;
94 + THROW_IF_FAILED(session.Get()->Authenticate(serverAddress.c_str(), username.c_str(), password.c_str(), &identityToken));
95 +
96 + // If the registry returned an identity token, use it. Otherwise fall back to username/password.
97 + if (identityToken && strlen(identityToken.get()) > 0)
98 + {
99 + return {TokenUsername, identityToken.get()};
100 + }
101 +
102 + return {username, password};
103 +}
104 +
105 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/RegistryService.h new
+37
@@ -0,0 +1,37 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RegistryService.h
8 +
9 +Abstract:
10 +
11 + This file contains the RegistryService definition
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include "ICredentialStorage.h"
17 +#include "SessionModel.h"
18 +
19 +namespace wsl::windows::wslc::services {
20 +
21 +// High-level registry authentication service.
22 +// Delegates credential persistence to ICredentialStorage (selected via OpenCredentialStorage).
23 +class RegistryService
24 +{
25 +public:
26 + static void Store(const std::string& serverAddress, const std::string& username, const std::string& secret);
27 + static std::string Get(const std::string& serverAddress);
28 + static void Erase(const std::string& serverAddress);
29 + static std::vector<std::wstring> List();
30 + static std::pair<std::string, std::string> Authenticate(
31 + wsl::windows::wslc::models::Session& session, const std::string& serverAddress, const std::string& username, const std::string& password);
32 +
33 + // Default registry server address used when no explicit server is provided.
34 + static constexpr auto DefaultServer = "https://index.docker.io/v1/";
35 +};
36 +
37 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/SessionModel.h new
+34
@@ -0,0 +1,34 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionModel.h
8 +
9 +Abstract:
10 +
11 + This file contains the SessionModel definition
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include <wslc.h>
17 +
18 +namespace wsl::windows::wslc::models {
19 +
20 +struct Session
21 +{
22 + explicit Session(wil::com_ptr<IWSLCSession> session) : m_session(std::move(session))
23 + {
24 + }
25 + IWSLCSession* Get() const noexcept
26 + {
27 + return m_session.get();
28 + }
29 +
30 +private:
31 + wil::com_ptr<IWSLCSession> m_session;
32 +};
33 +
34 +} // namespace wsl::windows::wslc::models
\ No newline at end of file
src/windows/wslc/services/SessionService.cpp new
+218
@@ -0,0 +1,218 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionService.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the SessionService implementation
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "SessionService.h"
17 +#include "ConsoleService.h"
18 +#include <wslc.h>
19 +#include <WSLCProcessLauncher.h>
20 +
21 +namespace wsl::windows::wslc::services {
22 +using namespace wsl::shared;
23 +using namespace wsl::windows::wslc::models;
24 +namespace wslutil = wsl::windows::common::wslutil;
25 +
26 +int SessionService::Attach(const std::wstring& sessionName)
27 +{
28 + wil::com_ptr<IWSLCSessionManager> manager;
29 + THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&manager)));
30 + wsl::windows::common::security::ConfigureForCOMImpersonation(manager.get());
31 +
32 + wil::com_ptr<IWSLCSession> session;
33 + HRESULT hr = manager->OpenSessionByName(sessionName.empty() ? nullptr : sessionName.c_str(), &session);
34 + if (FAILED(hr))
35 + {
36 + if (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND))
37 + {
38 + wslutil::PrintMessage(
39 + sessionName.empty() ? Localization::MessageWslcDefaultSessionNotFound()
40 + : Localization::MessageWslcSessionNotFound(sessionName.c_str()),
41 + stderr);
42 + return 1;
43 + }
44 +
45 + auto errorString = wsl::windows::common::wslutil::ErrorCodeToString(hr);
46 + wslutil::PrintMessage(
47 + Localization::MessageErrorCode(
48 + sessionName.empty() ? Localization::MessageWslcOpenDefaultSessionFailed()
49 + : Localization::MessageWslcOpenSessionFailed(sessionName.c_str()),
50 + errorString),
51 + stderr);
52 + return 1;
53 + }
54 +
55 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
56 +
57 + // Configure console for interactive usage.
58 + wsl::windows::common::ConsoleState console{};
59 + const auto windowSize = console.GetWindowSize();
60 +
61 + const std::string shell = "/bin/sh";
62 +
63 + // Launch with terminal fds (PTY).
64 + wsl::windows::common::WSLCProcessLauncher launcher{shell, {shell, "--login"}, {"TERM=xterm-256color"}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin};
65 + launcher.SetTtySize(windowSize.Y, windowSize.X);
66 + auto process = launcher.Launch(*session);
67 + auto tty = process.GetStdHandle(WSLCFDTty);
68 + auto updateTerminalSize = [&]() {
69 + const auto windowSize = console.GetWindowSize();
70 + LOG_IF_FAILED(process.Get().ResizeTty(windowSize.Y, windowSize.X));
71 + };
72 +
73 + // Start input relay thread to forward console input to TTY
74 + // Runs in parallel with output relay (main thread)
75 + auto exitEvent = wil::unique_event(wil::EventOptions::ManualReset);
76 + std::thread inputThread([&] {
77 + try
78 + {
79 + wsl::windows::common::relay::StandardInputRelay(
80 + GetStdHandle(STD_INPUT_HANDLE), tty.get(), updateTerminalSize, exitEvent.get());
81 + }
82 + catch (...)
83 + {
84 + exitEvent.SetEvent();
85 + }
86 + });
87 +
88 + auto joinInput = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
89 + exitEvent.SetEvent();
90 + if (inputThread.joinable())
91 + {
92 + inputThread.join();
93 + }
94 + });
95 +
96 + // Relay tty output -> console (blocks until output ends).
97 + wsl::windows::common::relay::InterruptableRelay(tty.get(), GetStdHandle(STD_OUTPUT_HANDLE), exitEvent.get());
98 +
99 + process.GetExitEvent().wait();
100 +
101 + auto exitCode = process.GetExitCode();
102 +
103 + wslutil::PrintMessage(wsl::shared::Localization::MessageWslcShellExited(string::MultiByteToWide(shell), static_cast<int>(exitCode)), stdout);
104 +
105 + return static_cast<int>(exitCode);
106 +}
107 +
108 +Session SessionService::CreateDefaultSession()
109 +{
110 + wil::com_ptr<IWSLCSessionManager> sessionManager;
111 + THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
112 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
113 +
114 + // Null Settings = default session with server-determined name and settings.
115 + wil::com_ptr<IWSLCSession> session;
116 + THROW_IF_FAILED(sessionManager->CreateSession(nullptr, WSLCSessionFlagsNone, &session));
117 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
118 + return Session(std::move(session));
119 +}
120 +
121 +int SessionService::Enter(const std::wstring& storagePath, const std::wstring& displayName)
122 +{
123 + THROW_HR_IF(E_INVALIDARG, storagePath.empty());
124 + THROW_HR_IF(E_INVALIDARG, displayName.empty());
125 +
126 + wil::com_ptr<IWSLCSessionManager> sessionManager;
127 + THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
128 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
129 +
130 + wil::com_ptr<IWSLCSession> session;
131 + THROW_IF_FAILED(sessionManager->EnterSession(displayName.c_str(), storagePath.c_str(), &session));
132 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
133 + wsl::windows::common::wslutil::PrintMessage(Localization::MessageWslcCreatedSession(displayName), stderr);
134 +
135 + const std::string shell = "/bin/sh";
136 + wsl::windows::common::WSLCProcessLauncher launcher{shell, {shell, "--login"}, {"TERM=xterm-256color"}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin};
137 +
138 + wsl::windows::common::ConsoleState console;
139 + const auto windowSize = console.GetWindowSize();
140 + launcher.SetTtySize(windowSize.Y, windowSize.X);
141 +
142 + return ConsoleService::AttachToCurrentConsole(launcher.Launch(*session.get()));
143 +}
144 +
145 +std::vector<SessionInformation> SessionService::List()
146 +{
147 + std::vector<SessionInformation> result;
148 + wil::com_ptr<IWSLCSessionManager> sessionManager;
149 + THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
150 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
151 +
152 + wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
153 + THROW_IF_FAILED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
154 + for (size_t i = 0; i < sessions.size(); ++i)
155 + {
156 + const auto& current = sessions[i];
157 + SessionInformation info{};
158 + info.CreatorPid = current.CreatorPid;
159 + info.SessionId = current.SessionId;
160 + info.DisplayName = current.DisplayName;
161 + result.emplace_back(info);
162 + }
163 +
164 + return result;
165 +}
166 +
167 +Session SessionService::OpenSession(const std::wstring& displayName)
168 +{
169 + wil::com_ptr<IWSLCSessionManager> sessionManager;
170 + THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
171 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
172 +
173 + wil::com_ptr<IWSLCSession> session;
174 + THROW_IF_FAILED(sessionManager->OpenSessionByName(displayName.c_str(), &session));
175 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
176 + return Session(std::move(session));
177 +}
178 +
179 +int SessionService::TerminateSession(const std::wstring& displayName)
180 +{
181 + wil::com_ptr<IWSLCSessionManager> sessionManager;
182 + THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
183 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
184 +
185 + wil::com_ptr<IWSLCSession> session;
186 + HRESULT hr = sessionManager->OpenSessionByName(displayName.empty() ? nullptr : displayName.c_str(), &session);
187 + if (FAILED(hr))
188 + {
189 + if (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND))
190 + {
191 + wslutil::PrintMessage(
192 + displayName.empty() ? Localization::MessageWslcDefaultSessionNotFound()
193 + : Localization::MessageWslcSessionNotFound(displayName.c_str()),
194 + stderr);
195 + return 1;
196 + }
197 +
198 + THROW_HR(hr);
199 + }
200 +
201 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
202 +
203 + hr = session->Terminate();
204 + if (FAILED(hr))
205 + {
206 + auto errorString = wsl::windows::common::wslutil::ErrorCodeToString(hr);
207 + wslutil::PrintMessage(
208 + Localization::MessageErrorCode(
209 + displayName.empty() ? Localization::MessageWslcTerminateDefaultSessionFailed()
210 + : Localization::MessageWslcTerminateSessionFailed(displayName.c_str()),
211 + errorString),
212 + stderr);
213 + return 1;
214 + }
215 +
216 + return 0;
217 +}
218 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/SessionService.h new
+37
@@ -0,0 +1,37 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionService.h
8 +
9 +Abstract:
10 +
11 + This file contains the SessionService definition
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include "SessionModel.h"
17 +#include <wslc.h>
18 +
19 +namespace wsl::windows::wslc::services {
20 +struct SessionInformation
21 +{
22 + ULONG SessionId;
23 + DWORD CreatorPid;
24 + std::wstring DisplayName;
25 +};
26 +
27 +struct SessionService
28 +{
29 + static int Attach(const std::wstring& name);
30 + // Creates a default session with server-determined name and settings.
31 + static wsl::windows::wslc::models::Session CreateDefaultSession();
32 + static int Enter(const std::wstring& storagePath, const std::wstring& displayName);
33 + static std::vector<SessionInformation> List();
34 + static wsl::windows::wslc::models::Session OpenSession(const std::wstring& displayName);
35 + static int TerminateSession(const std::wstring& displayName);
36 +};
37 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/VolumeModel.cpp new
+55
@@ -0,0 +1,55 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeModel.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the VolumeModel implementations
12 +
13 +--*/
14 +#include "precomp.h"
15 +#include "VolumeModel.h"
16 +
17 +using namespace wsl::shared;
18 +using namespace wsl::windows::common::string;
19 +
20 +namespace wsl::windows::wslc::models {
21 +
22 +std::pair<std::string, std::string> Label::Parse(const std::wstring& value)
23 +{
24 + std::pair<std::string, std::string> result{};
25 + auto pos = value.find('=');
26 + if (pos == std::wstring::npos)
27 + {
28 + result.first = WideToMultiByte(value);
29 + }
30 + else
31 + {
32 + result.first = WideToMultiByte(value.substr(0, pos));
33 + result.second = WideToMultiByte(value.substr(pos + 1));
34 + }
35 +
36 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::WSLCCLI_LabelKeyEmptyError(), result.first.empty());
37 + return result;
38 +}
39 +
40 +std::pair<std::string, std::string> DriverOption::Parse(const std::wstring& value)
41 +{
42 + std::pair<std::string, std::string> result{};
43 + auto pos = value.find('=');
44 + if (pos == std::wstring::npos)
45 + {
46 + result.first = WideToMultiByte(value);
47 + return result;
48 + }
49 +
50 + result.first = WideToMultiByte(value.substr(0, pos));
51 + result.second = WideToMultiByte(value.substr(pos + 1));
52 + return result;
53 +}
54 +
55 +} // namespace wsl::windows::wslc::models
src/windows/wslc/services/VolumeModel.h new
+40
@@ -0,0 +1,40 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeModel.h
8 +
9 +Abstract:
10 +
11 + This file contains the VolumeModel definitions
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "JsonUtils.h"
18 +#include <string>
19 +
20 +namespace wsl::windows::wslc::models {
21 +
22 +struct Label
23 +{
24 + static std::pair<std::string, std::string> Parse(const std::wstring& value);
25 +};
26 +
27 +struct DriverOption
28 +{
29 + static std::pair<std::string, std::string> Parse(const std::wstring& value);
30 +};
31 +
32 +struct CreateVolumeOptions
33 +{
34 + std::string Name;
35 + std::optional<std::string> Driver;
36 + std::vector<std::pair<std::string, std::string>> DriverOpts{};
37 + std::vector<std::pair<std::string, std::string>> Labels{};
38 +};
39 +
40 +} // namespace wsl::windows::wslc::models
src/windows/wslc/services/VolumeService.cpp new
+84
@@ -0,0 +1,84 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeService.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the VolumeService implementation
12 +
13 +--*/
14 +#include "VolumeService.h"
15 +#include <wslutil.h>
16 +#include <wslc.h>
17 +
18 +using namespace wsl::shared;
19 +using namespace wsl::shared::string;
20 +using namespace wsl::windows::common::wslutil;
21 +
22 +namespace wsl::windows::wslc::services {
23 +
24 +WSLCVolumeInformation VolumeService::Create(models::Session& session, const models::CreateVolumeOptions& createOptions)
25 +{
26 + WSLCVolumeOptions options{};
27 + options.Name = createOptions.Name.c_str();
28 + if (createOptions.Driver.has_value())
29 + {
30 + options.Driver = createOptions.Driver->c_str();
31 + }
32 +
33 + // Set driver options
34 + std::vector<KeyValuePair> driverOpts;
35 + for (const auto& option : createOptions.DriverOpts)
36 + {
37 + driverOpts.push_back({.Key = option.first.c_str(), .Value = option.second.c_str()});
38 + }
39 +
40 + // Set labels
41 + std::vector<KeyValuePair> labels;
42 + for (const auto& label : createOptions.Labels)
43 + {
44 + labels.push_back({.Key = label.first.c_str(), .Value = label.second.c_str()});
45 + }
46 +
47 + options.DriverOpts = driverOpts.data();
48 + options.DriverOptsCount = static_cast<ULONG>(driverOpts.size());
49 + options.Labels = labels.data();
50 + options.LabelsCount = static_cast<ULONG>(labels.size());
51 +
52 + WSLCVolumeInformation info{};
53 + THROW_IF_FAILED(session.Get()->CreateVolume(&options, &info));
54 + return info;
55 +}
56 +
57 +void VolumeService::Delete(models::Session& session, const std::string& name)
58 +{
59 + THROW_IF_FAILED(session.Get()->DeleteVolume(name.c_str()));
60 +}
61 +
62 +std::vector<WSLCVolumeInformation> VolumeService::List(models::Session& session)
63 +{
64 + wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> rawVolumes;
65 + ULONG count = 0;
66 + THROW_IF_FAILED(session.Get()->ListVolumes(&rawVolumes, &count));
67 +
68 + std::vector<WSLCVolumeInformation> volumes;
69 + volumes.reserve(count);
70 + for (auto ptr = rawVolumes.get(), end = rawVolumes.get() + count; ptr != end; ++ptr)
71 + {
72 + volumes.push_back(*ptr);
73 + }
74 +
75 + return volumes;
76 +}
77 +
78 +wsl::windows::common::wslc_schema::InspectVolume VolumeService::Inspect(models::Session& session, const std::string& name)
79 +{
80 + wil::unique_cotaskmem_ansistring output;
81 + THROW_IF_FAILED(session.Get()->InspectVolume(name.c_str(), &output));
82 + return FromJson<wsl::windows::common::wslc_schema::InspectVolume>(output.get());
83 +}
84 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/VolumeService.h new
+28
@@ -0,0 +1,28 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeService.h
8 +
9 +Abstract:
10 +
11 + This file contains the VolumeService definition
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include "SessionModel.h"
17 +#include "VolumeModel.h"
18 +#include <wslc_schema.h>
19 +
20 +namespace wsl::windows::wslc::services {
21 +struct VolumeService
22 +{
23 + static WSLCVolumeInformation Create(models::Session& session, const models::CreateVolumeOptions& createOptions);
24 + static void Delete(models::Session& session, const std::string& name);
25 + static std::vector<WSLCVolumeInformation> List(models::Session& session);
26 + static wsl::windows::common::wslc_schema::InspectVolume Inspect(models::Session& session, const std::string& name);
27 +};
28 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/WinCredStorage.cpp new
+113
@@ -0,0 +1,113 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WinCredStorage.cpp
8 +
9 +Abstract:
10 +
11 + Windows Credential Manager credential storage implementation.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "WinCredStorage.h"
17 +#include <wincred.h>
18 +
19 +using wsl::shared::Localization;
20 +
21 +using unique_credential = wil::unique_any<PCREDENTIALW, decltype(&CredFree), CredFree>;
22 +using unique_credential_array = wil::unique_any<PCREDENTIALW*, decltype(&CredFree), CredFree>;
23 +
24 +static constexpr auto WinCredPrefix = L"wslc-credential/";
25 +
26 +namespace wsl::windows::wslc::services {
27 +
28 +std::wstring WinCredStorage::TargetName(const std::string& serverAddress)
29 +{
30 + return std::wstring(WinCredPrefix) + wsl::shared::string::MultiByteToWide(serverAddress);
31 +}
32 +
33 +void WinCredStorage::Store(const std::string& serverAddress, const std::string& username, const std::string& secret)
34 +{
35 + auto targetName = TargetName(serverAddress);
36 + auto wideUsername = wsl::shared::string::MultiByteToWide(username);
37 +
38 + CREDENTIALW cred{};
39 + cred.Type = CRED_TYPE_GENERIC;
40 + cred.TargetName = const_cast<LPWSTR>(targetName.c_str());
41 + cred.UserName = const_cast<LPWSTR>(wideUsername.c_str());
42 + cred.CredentialBlobSize = static_cast<DWORD>(secret.size());
43 + cred.CredentialBlob = reinterpret_cast<LPBYTE>(const_cast<char*>(secret.data()));
44 + cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
45 +
46 + THROW_IF_WIN32_BOOL_FALSE(CredWriteW(&cred, 0));
47 +}
48 +
49 +std::pair<std::string, std::string> WinCredStorage::Get(const std::string& serverAddress)
50 +{
51 + auto targetName = TargetName(serverAddress);
52 +
53 + unique_credential cred;
54 + if (!CredReadW(targetName.c_str(), CRED_TYPE_GENERIC, 0, &cred))
55 + {
56 + THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND);
57 + return {};
58 + }
59 +
60 + if (cred.get()->CredentialBlobSize == 0 || cred.get()->CredentialBlob == nullptr)
61 + {
62 + return {};
63 + }
64 +
65 + std::string username;
66 + if (cred.get()->UserName)
67 + {
68 + username = wsl::shared::string::WideToMultiByte(cred.get()->UserName);
69 + }
70 +
71 + return {std::move(username), {reinterpret_cast<const char*>(cred.get()->CredentialBlob), cred.get()->CredentialBlobSize}};
72 +}
73 +
74 +void WinCredStorage::Erase(const std::string& serverAddress)
75 +{
76 + auto targetName = TargetName(serverAddress);
77 +
78 + if (!CredDeleteW(targetName.c_str(), CRED_TYPE_GENERIC, 0))
79 + {
80 + auto error = GetLastError();
81 + THROW_HR_WITH_USER_ERROR_IF(
82 + E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), error == ERROR_NOT_FOUND);
83 +
84 + THROW_WIN32(error);
85 + }
86 +}
87 +
88 +std::vector<std::wstring> WinCredStorage::List()
89 +{
90 + auto prefix = std::wstring(WinCredPrefix);
91 + auto filter = prefix + L"*";
92 +
93 + DWORD count = 0;
94 + unique_credential_array creds;
95 + if (!CredEnumerateW(filter.c_str(), 0, &count, &creds))
96 + {
97 + THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND);
98 + return {};
99 + }
100 +
101 + std::vector<std::wstring> result;
102 + result.reserve(count);
103 +
104 + for (DWORD i = 0; i < count; ++i)
105 + {
106 + std::wstring_view name(creds.get()[i]->TargetName);
107 + result.emplace_back(name.substr(prefix.size()));
108 + }
109 +
110 + return result;
111 +}
112 +
113 +} // namespace wsl::windows::wslc::services
src/windows/wslc/services/WinCredStorage.h new
+32
@@ -0,0 +1,32 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WinCredStorage.h
8 +
9 +Abstract:
10 +
11 + Windows Credential Manager credential storage backend.
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include "ICredentialStorage.h"
17 +
18 +namespace wsl::windows::wslc::services {
19 +
20 +class WinCredStorage final : public ICredentialStorage
21 +{
22 +public:
23 + void Store(const std::string& serverAddress, const std::string& username, const std::string& secret) override;
24 + std::pair<std::string, std::string> Get(const std::string& serverAddress) override;
25 + void Erase(const std::string& serverAddress) override;
26 + std::vector<std::wstring> List() override;
27 +
28 +private:
29 + static std::wstring TargetName(const std::string& serverAddress);
30 +};
31 +
32 +} // namespace wsl::windows::wslc::services
src/windows/wslc/tasks/ContainerTasks.cpp new
+435
@@ -0,0 +1,435 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerTasks.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of container command related execution logic.
12 +
13 +--*/
14 +#include "Argument.h"
15 +#include "ArgumentValidation.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerModel.h"
18 +#include "ContainerService.h"
19 +#include "ContainerTasks.h"
20 +#include "SessionModel.h"
21 +#include "SessionService.h"
22 +#include "TableOutput.h"
23 +#include "VolumeModel.h"
24 +#include <wil/result_macros.h>
25 +#include <wslc_schema.h>
26 +
27 +using namespace wsl::shared;
28 +using namespace wsl::windows::common;
29 +using namespace wsl::windows::common::string;
30 +using namespace wsl::windows::common::wslutil;
31 +using namespace wsl::windows::wslc::execution;
32 +using namespace wsl::windows::wslc::models;
33 +using namespace wsl::windows::wslc::services;
34 +
35 +namespace wsl::windows::wslc::task {
36 +
37 +static bool TryInspectContainer(Session& session, const std::string& containerId, std::optional<wslc_schema::InspectContainer>& inspectData)
38 +{
39 + try
40 + {
41 + inspectData = ContainerService::Inspect(session, containerId);
42 + return true;
43 + }
44 + catch (const wil::ResultException& ex)
45 + {
46 + if (ex.GetErrorCode() == WSLC_E_CONTAINER_NOT_FOUND)
47 + {
48 + PrintMessage(Localization::MessageWslcContainerNotFound(containerId.c_str()), stderr);
49 + return false;
50 + }
51 +
52 + throw;
53 + }
54 +}
55 +
56 +void AttachContainer::operator()(CLIExecutionContext& context) const
57 +{
58 + WI_ASSERT(context.Data.Contains(Data::Session));
59 + context.ExitCode = ContainerService::Attach(context.Data.Get<Data::Session>(), WideToMultiByte(m_containerId));
60 +}
61 +
62 +void CreateContainer(CLIExecutionContext& context)
63 +{
64 + WI_ASSERT(context.Data.Contains(Data::Session));
65 + WI_ASSERT(context.Args.Contains(ArgType::ImageId));
66 + WI_ASSERT(context.Data.Contains(Data::ContainerOptions));
67 + auto result = ContainerService::Create(
68 + context.Data.Get<Data::Session>(), WideToMultiByte(context.Args.Get<ArgType::ImageId>()), context.Data.Get<Data::ContainerOptions>());
69 + PrintMessage(MultiByteToWide(result.Id));
70 +}
71 +
72 +void ExecContainer(CLIExecutionContext& context)
73 +{
74 + WI_ASSERT(context.Data.Contains(Data::Session));
75 + WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
76 + WI_ASSERT(context.Data.Contains(Data::ContainerOptions));
77 + context.ExitCode = ContainerService::Exec(
78 + context.Data.Get<Data::Session>(), WideToMultiByte(context.Args.Get<ArgType::ContainerId>()), context.Data.Get<Data::ContainerOptions>());
79 +}
80 +
81 +void GetContainers(CLIExecutionContext& context)
82 +{
83 + WI_ASSERT(context.Data.Contains(Data::Session));
84 + auto& session = context.Data.Get<Data::Session>();
85 + context.Data.Add<Data::Containers>(ContainerService::List(session));
86 +}
87 +
88 +void InspectContainers(CLIExecutionContext& context)
89 +{
90 + WI_ASSERT(context.Data.Contains(Data::Session));
91 + auto& session = context.Data.Get<Data::Session>();
92 + auto containerIds = context.Args.GetAll<ArgType::ContainerId>();
93 + std::vector<wsl::windows::common::wslc_schema::InspectContainer> result;
94 + for (const auto& id : containerIds)
95 + {
96 + std::optional<wslc_schema::InspectContainer> inspectData;
97 + if (TryInspectContainer(session, WideToMultiByte(id), inspectData))
98 + {
99 + result.push_back(*inspectData);
100 + }
101 + else
102 + {
103 + context.ExitCode = 1;
104 + }
105 + }
106 +
107 + auto json = ToJson(result, c_jsonPrettyPrintIndent);
108 + PrintMessage(MultiByteToWide(json));
109 +}
110 +
111 +void KillContainers(CLIExecutionContext& context)
112 +{
113 + WI_ASSERT(context.Data.Contains(Data::Session));
114 + auto& session = context.Data.Get<Data::Session>();
115 + auto containerIds = context.Args.GetAll<ArgType::ContainerId>();
116 + WSLCSignal signal = WSLCSignalSIGKILL;
117 + if (context.Args.Contains(ArgType::Signal))
118 + {
119 + signal = validation::GetWSLCSignalFromString(context.Args.Get<ArgType::Signal>());
120 + }
121 +
122 + for (const auto& id : containerIds)
123 + {
124 + ContainerService::Kill(session, WideToMultiByte(id), signal);
125 + }
126 +}
127 +
128 +void ListContainers(CLIExecutionContext& context)
129 +{
130 + WI_ASSERT(context.Data.Contains(Data::Containers));
131 + auto& containers = context.Data.Get<Data::Containers>();
132 +
133 + // Filter by running state if --all is not specified
134 + if (!context.Args.Contains(ArgType::All))
135 + {
136 + auto shouldRemove = [](const ContainerInformation& container) {
137 + return container.State != WSLCContainerState::WslcContainerStateRunning;
138 + };
139 + containers.erase(std::remove_if(containers.begin(), containers.end(), shouldRemove), containers.end());
140 + }
141 +
142 + if (context.Args.Contains(ArgType::Quiet))
143 + {
144 + // Print only the container ids
145 + for (const auto& container : containers)
146 + {
147 + PrintMessage(MultiByteToWide(container.Id));
148 + }
149 +
150 + return;
151 + }
152 +
153 + FormatType format = FormatType::Table; // Default is table
154 + if (context.Args.Contains(ArgType::Format))
155 + {
156 + format = validation::GetFormatTypeFromString(context.Args.Get<ArgType::Format>());
157 + }
158 +
159 + switch (format)
160 + {
161 + case FormatType::Json:
162 + {
163 + auto json = ToJson(containers, c_jsonPrettyPrintIndent);
164 + PrintMessage(MultiByteToWide(json));
165 + break;
166 + }
167 + case FormatType::Table:
168 + {
169 + using Config = wsl::windows::wslc::ColumnWidthConfig;
170 + bool trunc = !context.Args.Contains(ArgType::NoTrunc);
171 +
172 + // Create table with or without column limits based on --no-trunc flag
173 + auto table =
174 + trunc ? wsl::windows::wslc::TableOutput<6>(
175 + {{{L"CONTAINER ID", {Config::NoLimit, 12, false}},
176 + {L"NAME", {Config::NoLimit, 20, true}},
177 + {L"IMAGE", {Config::NoLimit, 20, false}},
178 + {L"CREATED", {Config::NoLimit, Config::NoLimit, false}},
179 + {L"STATUS", {Config::NoLimit, Config::NoLimit, false}},
180 + {L"PORTS", {Config::NoLimit, Config::NoLimit, false}}}})
181 + : wsl::windows::wslc::TableOutput<6>({L"CONTAINER ID", L"NAME", L"IMAGE", L"CREATED", L"STATUS", L"PORTS"});
182 +
183 + // Add each container as a row
184 + for (const auto& container : containers)
185 + {
186 + table.OutputLine({
187 + MultiByteToWide(trunc ? TruncateId(container.Id) : container.Id),
188 + MultiByteToWide(container.Name),
189 + MultiByteToWide(container.Image),
190 + ContainerService::FormatRelativeTime(container.CreatedAt),
191 + ContainerService::ContainerStateToString(container.State, container.StateChangedAt),
192 + ContainerService::FormatPorts(container.State, container.Ports),
193 + });
194 + }
195 +
196 + table.Complete();
197 + break;
198 + }
199 + default:
200 + THROW_HR(E_UNEXPECTED);
201 + }
202 +}
203 +
204 +void RemoveContainers(CLIExecutionContext& context)
205 +{
206 + WI_ASSERT(context.Data.Contains(Data::Session));
207 + auto& session = context.Data.Get<Data::Session>();
208 + auto containerIds = context.Args.GetAll<ArgType::ContainerId>();
209 + bool force = context.Args.Contains(ArgType::Force);
210 + for (const auto& id : containerIds)
211 + {
212 + ContainerService::Delete(session, WideToMultiByte(id), force);
213 + }
214 +}
215 +
216 +void RunContainer(CLIExecutionContext& context)
217 +{
218 + WI_ASSERT(context.Data.Contains(Data::Session));
219 + WI_ASSERT(context.Args.Contains(ArgType::ImageId));
220 + WI_ASSERT(context.Data.Contains(Data::ContainerOptions));
221 + context.ExitCode = ContainerService::Run(
222 + context.Data.Get<Data::Session>(), WideToMultiByte(context.Args.Get<ArgType::ImageId>()), context.Data.Get<Data::ContainerOptions>());
223 +}
224 +
225 +void SetContainerOptionsFromArgs(CLIExecutionContext& context)
226 +{
227 + ContainerOptions options;
228 +
229 + if (context.Args.Contains(ArgType::Name))
230 + {
231 + options.Name = WideToMultiByte(context.Args.Get<ArgType::Name>());
232 + }
233 +
234 + if (context.Args.Contains(ArgType::TTY))
235 + {
236 + options.TTY = true;
237 + }
238 +
239 + if (context.Args.Contains(ArgType::Detach))
240 + {
241 + options.Detach = true;
242 + }
243 +
244 + if (context.Args.Contains(ArgType::Interactive))
245 + {
246 + options.Interactive = true;
247 + }
248 +
249 + if (context.Args.Contains(ArgType::Publish))
250 + {
251 + auto ports = context.Args.GetAll<ArgType::Publish>();
252 + options.Ports.reserve(options.Ports.size() + ports.size());
253 + for (const auto& port : ports)
254 + {
255 + options.Ports.emplace_back(WideToMultiByte(port));
256 + }
257 + }
258 +
259 + if (context.Args.Contains(ArgType::PublishAll))
260 + {
261 + options.PublishAll = true;
262 + }
263 +
264 + if (context.Args.Contains(ArgType::Volume))
265 + {
266 + auto volumes = context.Args.GetAll<ArgType::Volume>();
267 + options.Volumes.reserve(options.Volumes.size() + volumes.size());
268 + for (const auto& volume : volumes)
269 + {
270 + options.Volumes.emplace_back(volume);
271 + }
272 + }
273 +
274 + if (context.Args.Contains(ArgType::Remove))
275 + {
276 + options.Remove = true;
277 + }
278 +
279 + if (context.Args.Contains(ArgType::Command))
280 + {
281 + options.Arguments.emplace_back(WideToMultiByte(context.Args.Get<ArgType::Command>()));
282 + }
283 +
284 + if (context.Args.Contains(ArgType::EnvFile))
285 + {
286 + auto const& envFiles = context.Args.GetAll<ArgType::EnvFile>();
287 + for (const auto& envFile : envFiles)
288 + {
289 + auto parsedEnvVars = EnvironmentVariable::ParseFile(envFile);
290 + for (const auto& envVar : parsedEnvVars)
291 + {
292 + options.EnvironmentVariables.push_back(wsl::shared::string::WideToMultiByte(envVar));
293 + }
294 + }
295 + }
296 +
297 + if (context.Args.Contains(ArgType::Env))
298 + {
299 + auto const& envArgs = context.Args.GetAll<ArgType::Env>();
300 + for (const auto& arg : envArgs)
301 + {
302 + auto envVar = EnvironmentVariable::Parse(arg);
303 + if (envVar)
304 + {
305 + options.EnvironmentVariables.push_back(wsl::shared::string::WideToMultiByte(*envVar));
306 + }
307 + }
308 + }
309 +
310 + if (context.Args.Contains(ArgType::Entrypoint))
311 + {
312 + options.Entrypoint.push_back(WideToMultiByte(context.Args.Get<ArgType::Entrypoint>()));
313 + }
314 +
315 + if (context.Args.Contains(ArgType::Hostname))
316 + {
317 + options.Hostname = WideToMultiByte(context.Args.Get<ArgType::Hostname>());
318 + }
319 +
320 + if (context.Args.Contains(ArgType::Domainname))
321 + {
322 + options.Domainname = WideToMultiByte(context.Args.Get<ArgType::Domainname>());
323 + }
324 +
325 + if (context.Args.Contains(ArgType::DNS))
326 + {
327 + auto dnsServers = context.Args.GetAll<ArgType::DNS>();
328 + options.DnsServers.reserve(options.DnsServers.size() + dnsServers.size());
329 + for (const auto& value : dnsServers)
330 + {
331 + options.DnsServers.emplace_back(WideToMultiByte(value));
332 + }
333 + }
334 +
335 + if (context.Args.Contains(ArgType::DNSSearch))
336 + {
337 + auto dnsSearch = context.Args.GetAll<ArgType::DNSSearch>();
338 + options.DnsSearchDomains.reserve(options.DnsSearchDomains.size() + dnsSearch.size());
339 + for (const auto& value : dnsSearch)
340 + {
341 + options.DnsSearchDomains.emplace_back(WideToMultiByte(value));
342 + }
343 + }
344 +
345 + if (context.Args.Contains(ArgType::DNSOption))
346 + {
347 + auto dnsOptions = context.Args.GetAll<ArgType::DNSOption>();
348 + options.DnsOptions.reserve(options.DnsOptions.size() + dnsOptions.size());
349 + for (const auto& value : dnsOptions)
350 + {
351 + options.DnsOptions.emplace_back(WideToMultiByte(value));
352 + }
353 + }
354 +
355 + if (context.Args.Contains(ArgType::User))
356 + {
357 + options.User = WideToMultiByte(context.Args.Get<ArgType::User>());
358 + }
359 +
360 + if (context.Args.Contains(ArgType::TMPFS))
361 + {
362 + auto tmpfs = context.Args.GetAll<ArgType::TMPFS>();
363 + options.Tmpfs.reserve(options.Tmpfs.size() + tmpfs.size());
364 + for (const auto& value : tmpfs)
365 + {
366 + options.Tmpfs.emplace_back(WideToMultiByte(value));
367 + }
368 + }
369 +
370 + if (context.Args.Contains(ArgType::Label))
371 + {
372 + for (const auto& label : context.Args.GetAll<ArgType::Label>())
373 + {
374 + auto parsed = Label::Parse(label);
375 + options.Labels.emplace_back(parsed.first, parsed.second);
376 + }
377 + }
378 +
379 + if (context.Args.Contains(ArgType::ForwardArgs))
380 + {
381 + auto const& forwardArgs = context.Args.Get<ArgType::ForwardArgs>();
382 + options.Arguments.reserve(options.Arguments.size() + forwardArgs.size());
383 + for (const auto& arg : forwardArgs)
384 + {
385 + options.Arguments.emplace_back(WideToMultiByte(arg));
386 + }
387 + }
388 +
389 + if (context.Args.Contains(ArgType::WorkDir))
390 + {
391 + options.WorkingDirectory = WideToMultiByte(context.Args.Get<ArgType::WorkDir>());
392 + }
393 +
394 + context.Data.Add<Data::ContainerOptions>(std::move(options));
395 +}
396 +
397 +void StartContainer(CLIExecutionContext& context)
398 +{
399 + WI_ASSERT(context.Data.Contains(Data::Session));
400 + WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
401 + const auto& id = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
402 + context.ExitCode = ContainerService::Start(context.Data.Get<Data::Session>(), id, context.Args.Contains(ArgType::Attach));
403 +}
404 +
405 +void StopContainers(CLIExecutionContext& context)
406 +{
407 + WI_ASSERT(context.Data.Contains(Data::Session));
408 + auto& session = context.Data.Get<Data::Session>();
409 + auto containersToStop = context.Args.GetAll<ArgType::ContainerId>();
410 + StopContainerOptions options;
411 + if (context.Args.Contains(ArgType::Signal))
412 + {
413 + options.Signal = validation::GetWSLCSignalFromString(context.Args.Get<ArgType::Signal>());
414 + }
415 +
416 + if (context.Args.Contains(ArgType::Time))
417 + {
418 + options.Timeout = validation::GetIntegerFromString<LONG>(context.Args.Get<ArgType::Time>());
419 + }
420 +
421 + for (const auto& id : containersToStop)
422 + {
423 + ContainerService::Stop(context.Data.Get<Data::Session>(), WideToMultiByte(id), options);
424 + }
425 +}
426 +
427 +void ViewContainerLogs(CLIExecutionContext& context)
428 +{
429 + WI_ASSERT(context.Data.Contains(Data::Session));
430 + auto& session = context.Data.Get<Data::Session>();
431 + auto containerId = context.Args.Get<ArgType::ContainerId>();
432 + bool follow = context.Args.Contains(ArgType::Follow);
433 + ContainerService::Logs(session, WideToMultiByte(containerId), follow);
434 +}
435 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/ContainerTasks.h new
+45
@@ -0,0 +1,45 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerTasks.h
8 +
9 +Abstract:
10 +
11 + Declaration of container command execution tasks.
12 +
13 +--*/
14 +#pragma once
15 +#include "CLIExecutionContext.h"
16 +#include "Task.h"
17 +
18 +using wsl::windows::wslc::execution::CLIExecutionContext;
19 +
20 +namespace wsl::windows::wslc::task {
21 +
22 +struct AttachContainer : public Task
23 +{
24 + AttachContainer(const std::wstring& containerId) : m_containerId(containerId)
25 + {
26 + }
27 + void operator()(CLIExecutionContext& context) const override;
28 +
29 +private:
30 + std::wstring m_containerId;
31 +};
32 +
33 +void CreateContainer(CLIExecutionContext& context);
34 +void ExecContainer(CLIExecutionContext& context);
35 +void GetContainers(CLIExecutionContext& context);
36 +void InspectContainers(CLIExecutionContext& context);
37 +void KillContainers(CLIExecutionContext& context);
38 +void ListContainers(CLIExecutionContext& context);
39 +void RemoveContainers(CLIExecutionContext& context);
40 +void RunContainer(CLIExecutionContext& context);
41 +void SetContainerOptionsFromArgs(CLIExecutionContext& context);
42 +void StartContainer(CLIExecutionContext& context);
43 +void StopContainers(CLIExecutionContext& context);
44 +void ViewContainerLogs(CLIExecutionContext& context);
45 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/ImageTasks.cpp new
+291
@@ -0,0 +1,291 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageTasks.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of image command related execution logic.
12 +
13 +--*/
14 +#include "Argument.h"
15 +#include "ArgumentValidation.h"
16 +#include "BuildImageCallback.h"
17 +#include "CLIExecutionContext.h"
18 +#include "ContainerService.h"
19 +#include "ImageModel.h"
20 +#include "ImageService.h"
21 +#include "ImageTasks.h"
22 +#include "ImageProgressCallback.h"
23 +#include "TableOutput.h"
24 +#include "Task.h"
25 +#include <format>
26 +#include <wslutil.h>
27 +
28 +using namespace wsl::shared;
29 +using namespace wsl::windows::common;
30 +using namespace wsl::windows::common::string;
31 +using namespace wsl::windows::common::wslutil;
32 +using namespace wsl::windows::wslc::execution;
33 +using namespace wsl::windows::wslc::models;
34 +using namespace wsl::windows::wslc::services;
35 +
36 +namespace wsl::windows::wslc::task {
37 +
38 +static bool TryInspectImage(Session& session, const std::string& imageId, std::optional<wslc_schema::InspectImage>& inspectData)
39 +{
40 + try
41 + {
42 + inspectData = ImageService::Inspect(session, imageId);
43 + return true;
44 + }
45 + catch (const wil::ResultException& ex)
46 + {
47 + if (ex.GetErrorCode() == WSLC_E_IMAGE_NOT_FOUND)
48 + {
49 + PrintMessage(Localization::MessageWslcImageNotFound(imageId.c_str()), stderr);
50 + return false;
51 + }
52 +
53 + throw;
54 + }
55 +}
56 +
57 +void BuildImage(CLIExecutionContext& context)
58 +{
59 + WI_ASSERT(context.Data.Contains(Data::Session));
60 + WI_ASSERT(context.Args.Contains(ArgType::Path));
61 + auto& session = context.Data.Get<Data::Session>();
62 + auto& contextPath = context.Args.Get<ArgType::Path>();
63 +
64 + auto tags = context.Args.GetAll<ArgType::Tag>();
65 + auto buildArgs = context.Args.GetAll<ArgType::BuildArg>();
66 +
67 + std::wstring dockerfilePath;
68 + if (context.Args.Contains(ArgType::File))
69 + {
70 + dockerfilePath = context.Args.Get<ArgType::File>();
71 + }
72 +
73 + std::wstring target;
74 + if (context.Args.Contains(ArgType::BuildTarget))
75 + {
76 + target = context.Args.Get<ArgType::BuildTarget>();
77 + }
78 +
79 + PrintMessage(std::format(L"Building image from directory: {}\n", contextPath), stdout);
80 +
81 + WSLCBuildImageFlags flags = WSLCBuildImageFlagsNone;
82 + WI_SetFlagIf(flags, WSLCBuildImageFlagsVerbose, context.Args.Contains(ArgType::Verbose));
83 + WI_SetFlagIf(flags, WSLCBuildImageFlagsNoCache, context.Args.Contains(ArgType::NoCache));
84 + WI_SetFlagIf(flags, WSLCBuildImageFlagsPull, context.Args.Contains(ArgType::BuildPull));
85 +
86 + BuildImageCallback callback;
87 + services::ImageService::Build(session, contextPath, tags, buildArgs, dockerfilePath, target, flags, &callback, context.CreateCancelEvent());
88 +}
89 +
90 +void GetImages(CLIExecutionContext& context)
91 +{
92 + WI_ASSERT(context.Data.Contains(Data::Session));
93 + auto& session = context.Data.Get<Data::Session>();
94 + auto images = ImageService::List(session);
95 + context.Data.Add<Data::Images>(std::move(images));
96 +}
97 +
98 +void ListImages(CLIExecutionContext& context)
99 +{
100 + WI_ASSERT(context.Data.Contains(Data::Images));
101 + auto& images = context.Data.Get<Data::Images>();
102 +
103 + if (context.Args.Contains(ArgType::Quiet))
104 + {
105 + // Print only the image names.
106 + for (const auto& image : images)
107 + {
108 + PrintMessage(MultiByteToWide(image.Repository.value_or("<untagged>") + ":" + image.Tag.value_or("<untagged>")));
109 + }
110 +
111 + return;
112 + }
113 +
114 + FormatType format = FormatType::Table; // Default is table
115 + if (context.Args.Contains(ArgType::Format))
116 + {
117 + format = validation::GetFormatTypeFromString(context.Args.Get<ArgType::Format>());
118 + }
119 +
120 + switch (format)
121 + {
122 + case FormatType::Json:
123 + {
124 + auto json = ToJson(images, c_jsonPrettyPrintIndent);
125 + PrintMessage(MultiByteToWide(json));
126 + break;
127 + }
128 + case FormatType::Table:
129 + {
130 + using Config = wsl::windows::wslc::ColumnWidthConfig;
131 + bool trunc = !context.Args.Contains(ArgType::NoTrunc);
132 +
133 + // Create table — only IMAGE ID uses fixed width; other columns auto-size.
134 + // When --no-trunc is passed, IMAGE ID also shows full length via TruncateId().
135 + auto table = trunc ? wsl::windows::wslc::TableOutput<5>(
136 + {{{L"REPOSITORY", {Config::NoLimit, Config::NoLimit, false}},
137 + {L"TAG", {Config::NoLimit, Config::NoLimit, false}},
138 + {L"IMAGE ID", {12, 12, false}},
139 + {L"CREATED", {Config::NoLimit, Config::NoLimit, false}},
140 + {L"SIZE", {Config::NoLimit, Config::NoLimit, false}}}})
141 + : wsl::windows::wslc::TableOutput<5>({L"REPOSITORY", L"TAG", L"IMAGE ID", L"CREATED", L"SIZE"});
142 +
143 + for (const auto& image : images)
144 + {
145 + table.OutputLine({
146 + MultiByteToWide(image.Repository.value_or("<untagged>")),
147 + MultiByteToWide(image.Tag.value_or("<untagged>")),
148 + MultiByteToWide(TruncateId(image.Id, trunc)),
149 + ContainerService::FormatRelativeTime(image.Created > 0 ? static_cast<ULONGLONG>(image.Created) : 0),
150 + std::format(L"{:.2f} MB", static_cast<double>(image.Size) / WSLC_IMAGE_1MB),
151 + });
152 + }
153 +
154 + table.Complete();
155 + break;
156 + }
157 + default:
158 + THROW_HR(E_UNEXPECTED);
159 + }
160 +}
161 +
162 +void PullImage(CLIExecutionContext& context)
163 +{
164 + WI_ASSERT(context.Data.Contains(Data::Session));
165 + WI_ASSERT(context.Args.Contains(ArgType::ImageId));
166 + auto& session = context.Data.Get<Data::Session>();
167 + auto& imageId = context.Args.Get<ArgType::ImageId>();
168 +
169 + ImageProgressCallback callback;
170 + services::ImageService::Pull(session, WideToMultiByte(imageId), &callback);
171 +}
172 +
173 +void PushImage(CLIExecutionContext& context)
174 +{
175 + WI_ASSERT(context.Data.Contains(Data::Session));
176 + WI_ASSERT(context.Args.Contains(ArgType::ImageId));
177 + auto& session = context.Data.Get<Data::Session>();
178 + auto& imageId = context.Args.Get<ArgType::ImageId>();
179 +
180 + ImageProgressCallback callback;
181 + services::ImageService::Push(session, WideToMultiByte(imageId), &callback);
182 +}
183 +
184 +void DeleteImage(CLIExecutionContext& context)
185 +{
186 + WI_ASSERT(context.Data.Contains(Data::Session));
187 + WI_ASSERT(context.Args.Contains(ArgType::ImageId));
188 + auto& session = context.Data.Get<Data::Session>();
189 + auto& imageId = context.Args.Get<ArgType::ImageId>();
190 +
191 + bool force = context.Args.Contains(ArgType::ImageForce);
192 + bool noPrune = context.Args.Contains(ArgType::NoPrune);
193 + services::ImageService::Delete(session, WideToMultiByte(imageId), force, noPrune);
194 +}
195 +
196 +void LoadImage(CLIExecutionContext& context)
197 +{
198 + WI_ASSERT(context.Data.Contains(Data::Session));
199 + auto& session = context.Data.Get<Data::Session>();
200 +
201 + if (context.Args.Contains(ArgType::Input))
202 + {
203 + auto& input = context.Args.Get<ArgType::Input>();
204 + services::ImageService::Load(session, input);
205 + return;
206 + }
207 +
208 + // TODO Read from stdin if no input argument is provided.
209 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_ImageLoadNoInputError());
210 +}
211 +
212 +void InspectImages(CLIExecutionContext& context)
213 +{
214 + WI_ASSERT(context.Data.Contains(Data::Session));
215 + WI_ASSERT(context.Args.Contains(ArgType::ImageId));
216 + auto& session = context.Data.Get<Data::Session>();
217 + auto imageIds = context.Args.GetAll<ArgType::ImageId>();
218 +
219 + std::vector<wsl::windows::common::wslc_schema::InspectImage> result;
220 + for (const auto& id : imageIds)
221 + {
222 + std::optional<wslc_schema::InspectImage> inspectData;
223 + if (TryInspectImage(session, WideToMultiByte(id), inspectData))
224 + {
225 + result.push_back(*inspectData);
226 + }
227 + else
228 + {
229 + context.ExitCode = 1;
230 + }
231 + }
232 +
233 + auto json = ToJson(result, c_jsonPrettyPrintIndent);
234 + PrintMessage(MultiByteToWide(json));
235 +}
236 +
237 +void SaveImage(CLIExecutionContext& context)
238 +{
239 + WI_ASSERT(context.Data.Contains(Data::Session));
240 + WI_ASSERT(context.Args.Contains(ArgType::ImageId));
241 + auto& session = context.Data.Get<Data::Session>();
242 + auto& imageId = context.Args.Get<ArgType::ImageId>();
243 +
244 + if (context.Args.Contains(ArgType::Output))
245 + {
246 + auto& output = context.Args.Get<ArgType::Output>();
247 + services::ImageService::Save(session, WideToMultiByte(imageId), output, context.CreateCancelEvent());
248 + }
249 + else
250 + {
251 + auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
252 + if (wsl::windows::common::wslutil::IsConsoleHandle(stdoutHandle))
253 + {
254 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_ImageSaveStdoutIsTerminalError());
255 + }
256 +
257 + services::ImageService::Save(session, WideToMultiByte(imageId), stdoutHandle, context.CreateCancelEvent());
258 + }
259 +}
260 +
261 +void TagImage(CLIExecutionContext& context)
262 +{
263 + WI_ASSERT(context.Data.Contains(Data::Session));
264 + auto& session = context.Data.Get<Data::Session>();
265 + auto& source = context.Args.Get<ArgType::Source>();
266 + auto& target = context.Args.Get<ArgType::Target>();
267 + services::ImageService::Tag(session, WideToMultiByte(source), WideToMultiByte(target));
268 +}
269 +
270 +void PruneImages(CLIExecutionContext& context)
271 +{
272 + WI_ASSERT(context.Data.Contains(Data::Session));
273 + auto& session = context.Data.Get<Data::Session>();
274 +
275 + bool all = context.Args.Contains(ArgType::All);
276 + auto result = ImageService::Prune(session, all);
277 +
278 + for (const auto& image : result.UntaggedImages)
279 + {
280 + PrintMessage(Localization::WSLCCLI_ImagePruneUntagged(image));
281 + }
282 +
283 + for (const auto& image : result.DeletedImages)
284 + {
285 + PrintMessage(Localization::WSLCCLI_ImagePruneDeleted(image));
286 + }
287 +
288 + PrintMessage(L"");
289 + PrintMessage(Localization::WSLCCLI_ImagePruneSpaceReclaimed(static_cast<double>(result.SpaceReclaimed) / WSLC_IMAGE_1MB));
290 +}
291 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/ImageTasks.h new
+31
@@ -0,0 +1,31 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageTasks.h
8 +
9 +Abstract:
10 +
11 + Declaration of image command execution tasks.
12 +
13 +--*/
14 +#pragma once
15 +#include "CLIExecutionContext.h"
16 +
17 +using wsl::windows::wslc::execution::CLIExecutionContext;
18 +
19 +namespace wsl::windows::wslc::task {
20 +void BuildImage(CLIExecutionContext& context);
21 +void GetImages(CLIExecutionContext& context);
22 +void ListImages(CLIExecutionContext& context);
23 +void LoadImage(CLIExecutionContext& context);
24 +void PullImage(CLIExecutionContext& context);
25 +void PushImage(CLIExecutionContext& context);
26 +void DeleteImage(CLIExecutionContext& context);
27 +void InspectImages(CLIExecutionContext& context);
28 +void TagImage(CLIExecutionContext& context);
29 +void SaveImage(CLIExecutionContext& context);
30 +void PruneImages(CLIExecutionContext& context);
31 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/InspectTasks.cpp new
+107
@@ -0,0 +1,107 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + InspectTasks.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of inspection command related execution logic.
12 +--*/
13 +
14 +#include "Argument.h"
15 +#include "ArgumentValidation.h"
16 +#include "InspectTasks.h"
17 +#include "InspectModel.h"
18 +#include "ImageService.h"
19 +#include "VolumeService.h"
20 +#include "ContainerService.h"
21 +
22 +namespace wsl::windows::wslc::task {
23 +
24 +using namespace wsl::shared;
25 +using namespace wsl::windows::common;
26 +using namespace wsl::windows::common::string;
27 +using namespace wsl::windows::common::wslutil;
28 +using namespace wsl::windows::wslc::models;
29 +
30 +template <typename TInspectFn>
31 +static bool TryInspect(TInspectFn&& fn, HRESULT notFoundError)
32 +{
33 + try
34 + {
35 + fn();
36 + return true;
37 + }
38 + catch (const wil::ResultException& ex)
39 + {
40 + auto errorCode = ex.GetErrorCode();
41 + if (errorCode == notFoundError || errorCode == HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS) || errorCode == E_INVALIDARG)
42 + {
43 + return false;
44 + }
45 +
46 + throw;
47 + }
48 +}
49 +
50 +static bool TryInspectImage(wsl::windows::wslc::models::Session& session, const std::string& image, std::optional<wslc_schema::InspectImage>& result)
51 +{
52 + return TryInspect([&]() { result = services::ImageService::Inspect(session, image); }, WSLC_E_IMAGE_NOT_FOUND);
53 +}
54 +
55 +static bool TryInspectContainer(wsl::windows::wslc::models::Session& session, const std::string& containerId, std::optional<wslc_schema::InspectContainer>& result)
56 +{
57 + return TryInspect([&]() { result = services::ContainerService::Inspect(session, containerId); }, WSLC_E_CONTAINER_NOT_FOUND);
58 +}
59 +
60 +static bool TryInspectVolume(wsl::windows::wslc::models::Session& session, const std::string& volumeId, std::optional<wslc_schema::InspectVolume>& result)
61 +{
62 + return TryInspect([&]() { result = services::VolumeService::Inspect(session, volumeId); }, WSLC_E_VOLUME_NOT_FOUND);
63 +}
64 +
65 +void Inspect(CLIExecutionContext& context)
66 +{
67 + WI_ASSERT(context.Data.Contains(Data::Session));
68 + auto& session = context.Data.Get<Data::Session>();
69 + auto objectIds = context.Args.GetAll<ArgType::ObjectId>();
70 +
71 + nlohmann::json array = nlohmann::json::array();
72 + auto type = InspectType::All;
73 + if (context.Args.Contains(ArgType::Type))
74 + {
75 + type = validation::GetInspectTypeFromString(context.Args.Get<ArgType::Type>(), L"type");
76 + }
77 +
78 + for (const auto& objectId : objectIds)
79 + {
80 + auto id = WideToMultiByte(objectId);
81 + std::optional<wslc_schema::InspectContainer> container;
82 + std::optional<wslc_schema::InspectImage> image;
83 + std::optional<wslc_schema::InspectVolume> volume;
84 +
85 + if (WI_IsFlagSet(type, InspectType::Container) && TryInspectContainer(session, id, container))
86 + {
87 + array.push_back(std::move(*container));
88 + }
89 + else if (WI_IsFlagSet(type, InspectType::Image) && TryInspectImage(session, id, image))
90 + {
91 + array.push_back(std::move(*image));
92 + }
93 + else if (WI_IsFlagSet(type, InspectType::Volume) && TryInspectVolume(session, id, volume))
94 + {
95 + array.push_back(std::move(*volume));
96 + }
97 + else
98 + {
99 + PrintMessage(Localization::WSLCCLI_ObjectNotFoundError(objectId), stderr);
100 + context.ExitCode = 1;
101 + }
102 + }
103 +
104 + // Always print the array, even if it's empty or an error was encountered
105 + PrintMessage(MultiByteToWide(array.dump(c_jsonPrettyPrintIndent)));
106 +}
107 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/InspectTasks.h new
+22
@@ -0,0 +1,22 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + InspectTasks.h
8 +
9 +Abstract:
10 +
11 + Declaration of inspection command execution tasks.
12 +
13 +--*/
14 +#pragma once
15 +#include "CLIExecutionContext.h"
16 +#include "Task.h"
17 +
18 +using wsl::windows::wslc::execution::CLIExecutionContext;
19 +
20 +namespace wsl::windows::wslc::task {
21 +void Inspect(CLIExecutionContext& context);
22 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/RegistryTasks.cpp new
+66
@@ -0,0 +1,66 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RegistryTasks.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of registry command related execution logic.
12 +
13 +--*/
14 +#include "Argument.h"
15 +#include "CLIExecutionContext.h"
16 +#include "RegistryService.h"
17 +#include "RegistryTasks.h"
18 +#include "Task.h"
19 +
20 +using namespace wsl::shared;
21 +using namespace wsl::windows::common::string;
22 +using namespace wsl::windows::common::wslutil;
23 +using namespace wsl::windows::wslc::execution;
24 +using namespace wsl::windows::wslc::services;
25 +
26 +namespace wsl::windows::wslc::task {
27 +
28 +void Login(CLIExecutionContext& context)
29 +{
30 + WI_ASSERT(context.Data.Contains(Data::Session));
31 + WI_ASSERT(context.Args.Contains(ArgType::Username));
32 + WI_ASSERT(context.Args.Contains(ArgType::Password));
33 +
34 + auto& session = context.Data.Get<Data::Session>();
35 +
36 + auto username = WideToMultiByte(context.Args.Get<ArgType::Username>());
37 + auto password = WideToMultiByte(context.Args.Get<ArgType::Password>());
38 +
39 + auto serverAddress = std::string(RegistryService::DefaultServer);
40 +
41 + if (context.Args.Contains(ArgType::Server))
42 + {
43 + serverAddress = WideToMultiByte(context.Args.Get<ArgType::Server>());
44 + }
45 +
46 + auto [credUsername, credSecret] = RegistryService::Authenticate(session, serverAddress, username, password);
47 + RegistryService::Store(serverAddress, credUsername, credSecret);
48 +
49 + PrintMessage(Localization::WSLCCLI_LoginSucceeded());
50 +}
51 +
52 +void Logout(CLIExecutionContext& context)
53 +{
54 + auto serverAddress = std::string(RegistryService::DefaultServer);
55 +
56 + if (context.Args.Contains(ArgType::Server))
57 + {
58 + serverAddress = WideToMultiByte(context.Args.Get<ArgType::Server>());
59 + }
60 +
61 + RegistryService::Erase(serverAddress);
62 +
63 + PrintMessage(Localization::WSLCCLI_LogoutSucceeded(MultiByteToWide(serverAddress)));
64 +}
65 +
66 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/RegistryTasks.h new
+22
@@ -0,0 +1,22 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + RegistryTasks.h
8 +
9 +Abstract:
10 +
11 + Declaration of registry command execution tasks.
12 +
13 +--*/
14 +#pragma once
15 +#include "CLIExecutionContext.h"
16 +
17 +using wsl::windows::wslc::execution::CLIExecutionContext;
18 +
19 +namespace wsl::windows::wslc::task {
20 +void Login(CLIExecutionContext& context);
21 +void Logout(CLIExecutionContext& context);
22 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/SessionTasks.cpp new
+109
@@ -0,0 +1,109 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionTasks.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of session command related execution logic.
12 +
13 +--*/
14 +#include "Argument.h"
15 +#include "CLIExecutionContext.h"
16 +#include "SessionService.h"
17 +#include "SessionTasks.h"
18 +#include "TableOutput.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::shared;
22 +using namespace wsl::shared::string;
23 +using namespace wsl::windows::common::string;
24 +using namespace wsl::windows::common::wslutil;
25 +using namespace wsl::windows::wslc::execution;
26 +using namespace wsl::windows::wslc::services;
27 +
28 +namespace wsl::windows::wslc::task {
29 +
30 +void AttachToSession(CLIExecutionContext& context)
31 +{
32 + std::wstring sessionId;
33 + if (context.Args.Contains(ArgType::SessionId))
34 + {
35 + sessionId = context.Args.Get<ArgType::SessionId>();
36 + }
37 +
38 + context.ExitCode = SessionService::Attach(sessionId);
39 +}
40 +
41 +void CreateSession(CLIExecutionContext& context)
42 +{
43 + if (context.Args.Contains(ArgType::Session))
44 + {
45 + // User specified a session name — open only, don't create.
46 + const auto& sessionName = context.Args.Get<ArgType::Session>();
47 + context.Data.Add<Data::Session>(SessionService::OpenSession(sessionName));
48 + return;
49 + }
50 +
51 + // Create/open the default session.
52 + context.Data.Add<Data::Session>(SessionService::CreateDefaultSession());
53 +}
54 +
55 +void ListSessions(CLIExecutionContext& context)
56 +{
57 + auto sessions = SessionService::List();
58 + if (context.Args.Contains(ArgType::Verbose))
59 + {
60 + const wchar_t* plural = sessions.size() == 1 ? L"" : L"s";
61 + PrintMessage(std::format(L"[wslc] Found {} session{}", sessions.size(), plural), stdout);
62 + }
63 +
64 + TableOutput<3> table(
65 + {Localization::MessageWslcHeaderId(), Localization::MessageWslcHeaderCreatorPid(), Localization::MessageWslcHeaderDisplayName()});
66 +
67 + for (const auto& session : sessions)
68 + {
69 + table.OutputLine({
70 + std::to_wstring(session.SessionId),
71 + std::to_wstring(session.CreatorPid),
72 + session.DisplayName,
73 + });
74 + }
75 +
76 + table.Complete();
77 +}
78 +
79 +void TerminateSession(CLIExecutionContext& context)
80 +{
81 + std::wstring sessionId;
82 + if (context.Args.Contains(ArgType::SessionId))
83 + {
84 + sessionId = context.Args.Get<ArgType::SessionId>();
85 + }
86 +
87 + context.ExitCode = SessionService::TerminateSession(sessionId);
88 +}
89 +
90 +void EnterSession(CLIExecutionContext& context)
91 +{
92 + auto storagePath = std::filesystem::absolute(context.Args.Get<ArgType::StoragePath>());
93 +
94 + std::wstring sessionName;
95 + if (context.Args.Contains(ArgType::Name))
96 + {
97 + sessionName = context.Args.Get<ArgType::Name>();
98 + }
99 + else
100 + {
101 + GUID guid{};
102 + THROW_IF_FAILED(CoCreateGuid(&guid));
103 + sessionName = wsl::shared::string::GuidToString<wchar_t>(guid, wsl::shared::string::GuidToStringFlags::None);
104 + }
105 +
106 + context.ExitCode = SessionService::Enter(storagePath.wstring(), sessionName);
107 +}
108 +
109 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/SessionTasks.h new
+25
@@ -0,0 +1,25 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + DiagTasks.h
8 +
9 +Abstract:
10 +
11 + Declaration of diag command execution tasks.
12 +
13 +--*/
14 +#pragma once
15 +#include "CLIExecutionContext.h"
16 +
17 +using wsl::windows::wslc::execution::CLIExecutionContext;
18 +
19 +namespace wsl::windows::wslc::task {
20 +void AttachToSession(CLIExecutionContext& context);
21 +void CreateSession(CLIExecutionContext& context);
22 +void EnterSession(CLIExecutionContext& context);
23 +void ListSessions(CLIExecutionContext& context);
24 +void TerminateSession(CLIExecutionContext& context);
25 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/Task.h new
+58
@@ -0,0 +1,58 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + Task.h
8 +
9 +Abstract:
10 +
11 + Declaration of a task for function composition and chaining.
12 +
13 +--*/
14 +#pragma once
15 +#include "CLIExecutionContext.h"
16 +#include <functional>
17 +
18 +using namespace wsl::windows::wslc::execution;
19 +
20 +namespace wsl::windows::wslc::task {
21 +
22 +struct Task
23 +{
24 + using Func = std::function<void(CLIExecutionContext&)>;
25 +
26 + Task(void (*f)(CLIExecutionContext&)) : m_func(f)
27 + {
28 + }
29 +
30 + Task(Func f) : m_func(std::move(f))
31 + {
32 + }
33 +
34 + Task() = default;
35 + virtual ~Task() = default;
36 +
37 + Task(const Task&) = default;
38 + Task& operator=(const Task&) = default;
39 + virtual void operator()(CLIExecutionContext& context) const
40 + {
41 + m_func(context);
42 + }
43 +
44 +private:
45 + Func m_func = nullptr;
46 +};
47 +
48 +inline CLIExecutionContext& operator<<(CLIExecutionContext& context, const Task& task)
49 +{
50 + return task(context), context;
51 +}
52 +
53 +inline CLIExecutionContext& operator<<(CLIExecutionContext& context, void (*f)(CLIExecutionContext&))
54 +{
55 + return context << Task(f);
56 +}
57 +
58 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/VolumeTasks.cpp new
+197
@@ -0,0 +1,197 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeTasks.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of volume command related execution logic.
12 +
13 +--*/
14 +#include "Argument.h"
15 +#include "ArgumentValidation.h"
16 +#include "CLIExecutionContext.h"
17 +#include "VolumeModel.h"
18 +#include "VolumeService.h"
19 +#include "VolumeTasks.h"
20 +#include "TableOutput.h"
21 +#include <wslc_schema.h>
22 +
23 +using namespace wsl::shared;
24 +using namespace wsl::windows::common;
25 +using namespace wsl::windows::common::string;
26 +using namespace wsl::windows::common::wslutil;
27 +using namespace wsl::windows::wslc::execution;
28 +using namespace wsl::windows::wslc::models;
29 +using namespace wsl::windows::wslc::services;
30 +
31 +namespace wsl::windows::wslc::task {
32 +
33 +static bool TryInspectVolume(Session& session, const std::string& volumeName, std::optional<wslc_schema::InspectVolume>& inspectData)
34 +{
35 + try
36 + {
37 + inspectData = VolumeService::Inspect(session, volumeName);
38 + return true;
39 + }
40 + catch (const wil::ResultException& ex)
41 + {
42 + if (ex.GetErrorCode() == WSLC_E_VOLUME_NOT_FOUND)
43 + {
44 + PrintMessage(Localization::MessageWslcVolumeNotFound(volumeName.c_str()), stderr);
45 + return false;
46 + }
47 +
48 + throw;
49 + }
50 +}
51 +
52 +static bool TryDeleteVolume(Session& session, const std::string& volumeName)
53 +{
54 + try
55 + {
56 + VolumeService::Delete(session, volumeName);
57 + return true;
58 + }
59 + catch (const wil::ResultException& ex)
60 + {
61 + if (ex.GetErrorCode() == WSLC_E_VOLUME_NOT_FOUND)
62 + {
63 + PrintMessage(Localization::MessageWslcVolumeNotFound(volumeName.c_str()), stderr);
64 + return false;
65 + }
66 +
67 + throw;
68 + }
69 +}
70 +
71 +void CreateVolume(CLIExecutionContext& context)
72 +{
73 + WI_ASSERT(context.Data.Contains(Data::Session));
74 +
75 + models::CreateVolumeOptions options{};
76 + if (context.Args.Contains(ArgType::VolumeName))
77 + {
78 + options.Name = WideToMultiByte(context.Args.Get<ArgType::VolumeName>());
79 + }
80 +
81 + for (const auto& option : context.Args.GetAll<ArgType::Options>())
82 + {
83 + auto parsed = DriverOption::Parse(option);
84 + options.DriverOpts.emplace_back(parsed.first, parsed.second);
85 + }
86 +
87 + for (const auto& label : context.Args.GetAll<ArgType::Label>())
88 + {
89 + auto parsed = Label::Parse(label);
90 + options.Labels.emplace_back(parsed.first, parsed.second);
91 + }
92 +
93 + if (context.Args.Contains(ArgType::Driver))
94 + {
95 + options.Driver = WideToMultiByte(context.Args.Get<ArgType::Driver>());
96 + }
97 +
98 + auto result = VolumeService::Create(context.Data.Get<Data::Session>(), options);
99 + PrintMessage(MultiByteToWide(result.Name));
100 +}
101 +
102 +void DeleteVolumes(CLIExecutionContext& context)
103 +{
104 + WI_ASSERT(context.Data.Contains(Data::Session));
105 + auto& session = context.Data.Get<Data::Session>();
106 + auto volumeNames = context.Args.GetAll<ArgType::VolumeName>();
107 + for (const auto& name : volumeNames)
108 + {
109 + if (TryDeleteVolume(session, WideToMultiByte(name)))
110 + {
111 + PrintMessage(name);
112 + }
113 + else
114 + {
115 + context.ExitCode = 1;
116 + }
117 + }
118 +}
119 +
120 +void GetVolumes(CLIExecutionContext& context)
121 +{
122 + WI_ASSERT(context.Data.Contains(Data::Session));
123 + auto& session = context.Data.Get<Data::Session>();
124 + context.Data.Add<Data::Volumes>(VolumeService::List(session));
125 +}
126 +
127 +void InspectVolumes(CLIExecutionContext& context)
128 +{
129 + WI_ASSERT(context.Data.Contains(Data::Session));
130 + auto& session = context.Data.Get<Data::Session>();
131 + auto volumeNames = context.Args.GetAll<ArgType::VolumeName>();
132 + std::vector<wsl::windows::common::wslc_schema::InspectVolume> result;
133 + for (const auto& name : volumeNames)
134 + {
135 + std::optional<wslc_schema::InspectVolume> inspectData;
136 + if (TryInspectVolume(session, WideToMultiByte(name), inspectData))
137 + {
138 + result.push_back(*inspectData);
139 + }
140 + else
141 + {
142 + context.ExitCode = 1;
143 + }
144 + }
145 +
146 + auto json = ToJson(result, c_jsonPrettyPrintIndent);
147 + PrintMessage(MultiByteToWide(json));
148 +}
149 +
150 +void ListVolumes(CLIExecutionContext& context)
151 +{
152 + WI_ASSERT(context.Data.Contains(Data::Volumes));
153 + auto& volumes = context.Data.Get<Data::Volumes>();
154 +
155 + if (context.Args.Contains(ArgType::Quiet))
156 + {
157 + for (const auto& volume : volumes)
158 + {
159 + PrintMessage(MultiByteToWide(volume.Name));
160 + }
161 +
162 + return;
163 + }
164 +
165 + FormatType format = FormatType::Table;
166 + if (context.Args.Contains(ArgType::Format))
167 + {
168 + format = validation::GetFormatTypeFromString(context.Args.Get<ArgType::Format>());
169 + }
170 +
171 + switch (format)
172 + {
173 + case FormatType::Json:
174 + {
175 + auto json = ToJson(volumes, c_jsonPrettyPrintIndent);
176 + PrintMessage(MultiByteToWide(json));
177 + break;
178 + }
179 + case FormatType::Table:
180 + {
181 + auto table = wsl::windows::wslc::TableOutput<2>({L"DRIVER", L"VOLUME NAME"});
182 + for (const auto& volume : volumes)
183 + {
184 + table.OutputLine({
185 + MultiByteToWide(volume.Driver),
186 + MultiByteToWide(volume.Name),
187 + });
188 + }
189 +
190 + table.Complete();
191 + break;
192 + }
193 + default:
194 + THROW_HR(E_UNEXPECTED);
195 + }
196 +}
197 +} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/VolumeTasks.h new
+24
@@ -0,0 +1,24 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VolumeTasks.h
8 +
9 +Abstract:
10 +
11 + Declaration of volume command execution tasks.
12 +
13 +--*/
14 +#pragma once
15 +#include "CLIExecutionContext.h"
16 +
17 +namespace wsl::windows::wslc::task {
18 +
19 +void CreateVolume(wsl::windows::wslc::execution::CLIExecutionContext& context);
20 +void DeleteVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context);
21 +void GetVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context);
22 +void InspectVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context);
23 +void ListVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context);
24 +} // namespace wsl::windows::wslc::task
src/windows/wslcsession/CMakeLists.txt new
+62
@@ -0,0 +1,62 @@
1 +set(SOURCES
2 + main.cpp
3 + main.rc
4 + application.manifest
5 +
6 + # Session factory and service reference
7 + WSLCSessionFactory.cpp
8 + WSLCSessionReference.cpp
9 +
10 + # Session and container implementation
11 + WSLCSession.cpp
12 + WSLCContainer.cpp
13 + WSLCVirtualMachine.cpp
14 +
15 + # Process management
16 + WSLCProcess.cpp
17 + WSLCProcessControl.cpp
18 + WSLCProcessIO.cpp
19 +
20 + # Volume management
21 + WSLCVhdVolume.cpp
22 + WSLCGuestVolume.cpp
23 +
24 + # Supporting classes
25 + ContainerEventTracker.cpp
26 + DockerHTTPClient.cpp
27 + IORelay.cpp
28 + ServiceProcessLauncher.cpp
29 + )
30 +
31 +set(HEADERS
32 + ContainerEventTracker.h
33 + DockerHTTPClient.h
34 + IORelay.h
35 + ServiceProcessLauncher.h
36 + WSLCContainer.h
37 + WSLCContainerMetadata.h
38 + WSLCProcess.h
39 + WSLCProcessControl.h
40 + WSLCProcessIO.h
41 + WSLCSession.h
42 + WSLCSessionFactory.h
43 + WSLCSessionReference.h
44 + WSLCVirtualMachine.h
45 + WSLCVhdVolume.h
46 + WSLCGuestVolume.h
47 + IWSLCVolume.h
48 + WSLCVolumeMetadata.h)
49 +
50 +add_executable(wslcsession WIN32 ${SOURCES} ${HEADERS})
51 +add_dependencies(wslcsession wslserviceidl)
52 +add_compile_definitions(__WRL_CLASSIC_COM__)
53 +add_compile_definitions(USE_COM_CONTEXT_DEF=1)
54 +target_link_libraries(wslcsession
55 + ${COMMON_LINK_LIBRARIES}
56 + common
57 + legacy_stdio_definitions
58 + VirtDisk.lib
59 + Crypt32.lib)
60 +
61 +target_precompile_headers(wslcsession REUSE_FROM common)
62 +set_target_properties(wslcsession PROPERTIES FOLDER windows)
src/windows/wslcsession/ContainerEventTracker.cpp new
+194
@@ -0,0 +1,194 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerEventTracker.cpp
8 +
9 +Abstract:
10 +
11 + Contains the implementation of ContainerEventTracker.
12 +
13 +--*/
14 +#include "precomp.h"
15 +#include "ContainerEventTracker.h"
16 +#include "WSLCVirtualMachine.h"
17 +#include <nlohmann/json.hpp>
18 +
19 +using wsl::windows::common::relay::MultiHandleWait;
20 +using wsl::windows::service::wslc::ContainerEventTracker;
21 +using wsl::windows::service::wslc::DockerHTTPClient;
22 +using wsl::windows::service::wslc::WSLCVirtualMachine;
23 +
24 +ContainerEventTracker::ContainerTrackingReference::ContainerTrackingReference(ContainerEventTracker* tracker, size_t id) noexcept :
25 + m_tracker(tracker), m_id(id)
26 +{
27 +}
28 +
29 +ContainerEventTracker::ContainerTrackingReference& ContainerEventTracker::ContainerTrackingReference::operator=(
30 + ContainerEventTracker::ContainerTrackingReference&& other) noexcept
31 +{
32 + Reset();
33 + m_id = other.m_id;
34 + m_tracker = other.m_tracker;
35 +
36 + other.m_tracker = nullptr;
37 + other.m_id = {};
38 +
39 + return *this;
40 +}
41 +
42 +void ContainerEventTracker::ContainerTrackingReference::Reset() noexcept
43 +{
44 + if (m_tracker != nullptr)
45 + {
46 + m_tracker->UnregisterContainerStateUpdates(m_id);
47 + m_tracker = nullptr;
48 + m_id = {};
49 + }
50 +}
51 +
52 +ContainerEventTracker::ContainerTrackingReference::ContainerTrackingReference(ContainerTrackingReference&& other) noexcept :
53 + m_id(other.m_id), m_tracker(other.m_tracker)
54 +{
55 + other.m_tracker = nullptr;
56 + other.m_id = {};
57 +}
58 +
59 +ContainerEventTracker::ContainerTrackingReference::~ContainerTrackingReference() noexcept
60 +{
61 + Reset();
62 +}
63 +
64 +ContainerEventTracker::ContainerEventTracker(DockerHTTPClient& dockerClient, ULONG sessionId, IORelay& relay) :
65 + m_sessionId(sessionId)
66 +{
67 + auto onChunk = [this](const gsl::span<char>& buffer) {
68 + if (!buffer.empty()) // docker inserts empty lines between events, skip those.
69 + {
70 + try
71 + {
72 + OnEvent(std::string_view(buffer.data(), buffer.size()));
73 + }
74 + catch (...)
75 + {
76 + WSL_LOG(
77 + "DockerEventParseError",
78 + TraceLoggingValue(buffer.data(), "Data"),
79 + TraceLoggingValue(wil::ResultFromCaughtException(), "Error"),
80 + TraceLoggingValue(m_sessionId, "SessionId"));
81 + }
82 + }
83 + };
84 +
85 + auto socket = dockerClient.MonitorEvents();
86 +
87 + relay.AddHandle(std::make_unique<common::relay::HTTPChunkBasedReadHandle>(std::move(socket), std::move(onChunk)));
88 +}
89 +
90 +ContainerEventTracker::~ContainerEventTracker()
91 +{
92 + // N.B. No callback should be left when the tracker is destroyed.
93 + WI_ASSERT(m_callbacks.empty());
94 +}
95 +
96 +void ContainerEventTracker::OnEvent(const std::string_view& event)
97 +{
98 + WSL_LOG(
99 + "DockerEvent",
100 + TraceLoggingCountedString(
101 + event.data(), static_cast<UINT16>(std::min(event.size(), static_cast<size_t>(USHRT_MAX))), "Data"),
102 + TraceLoggingValue(m_sessionId, "SessionId"));
103 +
104 + static std::map<std::string, ContainerEvent> events{
105 + {"start", ContainerEvent::Start}, {"die", ContainerEvent::Stop}, {"exec_die", ContainerEvent::ExecDied}};
106 +
107 + auto parsed = nlohmann::json::parse(event);
108 +
109 + auto action = parsed.find("Action");
110 + auto actor = parsed.find("Actor");
111 +
112 + THROW_HR_IF_MSG(
113 + E_INVALIDARG,
114 + action == parsed.end() || actor == parsed.end(),
115 + "Failed to parse json: %.*hs",
116 + static_cast<int>(event.size()),
117 + event.data());
118 +
119 + auto it = events.find(action->get<std::string>());
120 + if (it == events.end())
121 + {
122 + return; // Event is not tracked, dropped.
123 + }
124 +
125 + auto id = actor->find("ID");
126 + THROW_HR_IF_MSG(E_INVALIDARG, id == actor->end(), "Failed to parse json: %.*hs", static_cast<int>(event.size()), event.data());
127 +
128 + auto containerId = id->get<std::string>();
129 +
130 + std::optional<int> exitCode;
131 + std::optional<std::string> execId;
132 + auto attributes = actor->find("Attributes");
133 + if (attributes != actor->end())
134 + {
135 + auto exitCodeEntry = attributes->find("exitCode");
136 + if (exitCodeEntry != attributes->end())
137 + {
138 + exitCode = std::stoi(exitCodeEntry->get<std::string>());
139 + }
140 +
141 + auto execIdEntry = attributes->find("execID");
142 + if (execIdEntry != attributes->end())
143 + {
144 + execId = execIdEntry->get<std::string>();
145 + }
146 + }
147 +
148 + auto timeEntry = parsed.find("time");
149 + THROW_HR_IF_MSG(
150 + E_INVALIDARG, timeEntry == parsed.end(), "Failed to parse time from event: %.*hs", static_cast<int>(event.size()), event.data());
151 + std::uint64_t eventTime = timeEntry->get<std::uint64_t>();
152 +
153 + std::lock_guard lock{m_lock};
154 +
155 + for (const auto& e : m_callbacks)
156 + {
157 + if (e.ContainerId == containerId && (!e.ExecId.has_value() || e.ExecId == execId))
158 + {
159 + e.Callback(it->second, exitCode, eventTime);
160 + }
161 + }
162 +}
163 +
164 +ContainerEventTracker::ContainerTrackingReference ContainerEventTracker::RegisterContainerStateUpdates(
165 + const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept
166 +{
167 + std::lock_guard lock{m_lock};
168 +
169 + auto id = m_callbackId++;
170 + m_callbacks.emplace_back(id, ContainerId, std::optional<std::string>{}, std::move(Callback));
171 +
172 + return ContainerTrackingReference{this, id};
173 +}
174 +
175 +ContainerEventTracker::ContainerTrackingReference ContainerEventTracker::RegisterExecStateUpdates(
176 + const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept
177 +{
178 + std::lock_guard lock{m_lock};
179 +
180 + auto id = m_callbackId++;
181 + m_callbacks.emplace_back(id, ContainerId, ExecId, std::move(Callback));
182 +
183 + return ContainerTrackingReference{this, id};
184 +}
185 +
186 +void ContainerEventTracker::UnregisterContainerStateUpdates(size_t Id) noexcept
187 +{
188 + std::lock_guard lock{m_lock};
189 +
190 + auto remove = std::ranges::remove_if(m_callbacks, [Id](auto& entry) { return entry.CallbackId == Id; });
191 + WI_ASSERT(remove.size() == 1);
192 +
193 + m_callbacks.erase(remove.begin(), remove.end());
194 +}
\ No newline at end of file
src/windows/wslcsession/ContainerEventTracker.h new
+86
@@ -0,0 +1,86 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerEventTracker.h
8 +
9 +Abstract:
10 +
11 + Contains the definition for ContainerEventTracker.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "DockerHTTPClient.h"
18 +#include "IORelay.h"
19 +
20 +namespace wsl::windows::service::wslc {
21 +
22 +class WSLCVirtualMachine;
23 +
24 +enum class ContainerEvent
25 +{
26 + Create,
27 + Start,
28 + Stop,
29 + Exit,
30 + Destroy,
31 + ExecDied
32 +};
33 +
34 +class ContainerEventTracker
35 +{
36 +public:
37 + NON_COPYABLE(ContainerEventTracker);
38 + NON_MOVABLE(ContainerEventTracker);
39 +
40 + struct ContainerTrackingReference
41 + {
42 + NON_COPYABLE(ContainerTrackingReference);
43 +
44 + ContainerTrackingReference() = default;
45 + ContainerTrackingReference(ContainerEventTracker* tracker, size_t id) noexcept;
46 + ContainerTrackingReference(ContainerTrackingReference&& other) noexcept;
47 + ~ContainerTrackingReference() noexcept;
48 +
49 + ContainerTrackingReference& operator=(ContainerTrackingReference&&) noexcept;
50 +
51 + void Reset() noexcept;
52 +
53 + size_t m_id;
54 + ContainerEventTracker* m_tracker = nullptr;
55 + };
56 +
57 + using ContainerStateChangeCallback = std::function<void(ContainerEvent, std::optional<int>, std::uint64_t)>;
58 +
59 + ContainerEventTracker(DockerHTTPClient& dockerClient, ULONG sessionId, IORelay& relay);
60 + ~ContainerEventTracker();
61 +
62 + void Stop();
63 +
64 + ContainerTrackingReference RegisterContainerStateUpdates(const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept;
65 + ContainerTrackingReference RegisterExecStateUpdates(const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept;
66 + void UnregisterContainerStateUpdates(size_t Id) noexcept;
67 +
68 +private:
69 + void OnEvent(const std::string_view& event);
70 + void Run(wil::unique_socket&& Socket);
71 +
72 + struct Callback
73 + {
74 + size_t CallbackId;
75 + std::string ContainerId;
76 + std::optional<std::string> ExecId;
77 + ContainerStateChangeCallback Callback;
78 + };
79 +
80 + std::vector<Callback> m_callbacks;
81 +
82 + ULONG m_sessionId{};
83 + std::recursive_mutex m_lock;
84 + std::atomic<size_t> m_callbackId{0};
85 +};
86 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/DockerHTTPClient.cpp new
+840
@@ -0,0 +1,840 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + DockerHTTPClient.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the implementation of the Docker HTTP client.
12 + This class is designed to wrap calls to the docker API over a socket channel.
13 +
14 + The flow of an HTTP request is:
15 +
16 + - Create a new hvsocket channel by sending a WSLC_FORK message to init.
17 + - Connect the new socket to the docker unix socket server via WSLC_UNIX_CONNECT
18 + - Once connected, send the HTTP request over that socket.
19 +
20 + Some HTTP requests have simple response bodies that can be read right away, and some others upgrade
21 + the connection to TCP (like attaching to a process stdio, importing a tar, ...). For those,
22 + we return the socket without reading the response body, so the caller can interact directly with the stream.
23 +
24 +--*/
25 +
26 +#include "precomp.h"
27 +
28 +#include <winrt/Windows.Foundation.h>
29 +#include "DockerHTTPClient.h"
30 +
31 +namespace http = boost::beast::http;
32 +using boost::beast::http::verb;
33 +using wsl::windows::common::docker_schema::EmptyRequest;
34 +using wsl::windows::common::relay::HandleWrapper;
35 +using wsl::windows::common::relay::MultiHandleWait;
36 +using wsl::windows::service::wslc::DockerHTTPClient;
37 +using namespace wsl::windows::common;
38 +
39 +namespace {
40 +
41 +bool IsResponseChunked(const http::response_parser<http::buffer_body>::value_type& response)
42 +{
43 + auto transferEncoding = response.find(http::field::transfer_encoding);
44 + if (transferEncoding == response.end())
45 + {
46 + return false;
47 + }
48 +
49 + if (transferEncoding->value() != "chunked")
50 + {
51 + THROW_HR_MSG(E_UNEXPECTED, "Unknown transfer encoding: %hs", std::string(transferEncoding->value()).c_str());
52 + }
53 +
54 + return true;
55 +}
56 +template <typename TFilters>
57 +nlohmann::json PruneFiltersToJson(const TFilters& filters)
58 +{
59 + nlohmann::json j;
60 +
61 + if constexpr (requires { filters.dangling; })
62 + {
63 + if (filters.dangling.has_value())
64 + {
65 + j["dangling"] = nlohmann::json::array({filters.dangling.value() ? "true" : "false"});
66 + }
67 + }
68 +
69 + if (filters.until.has_value())
70 + {
71 + j["until"] = nlohmann::json::array({std::to_string(filters.until.value())});
72 + }
73 +
74 + if (!filters.presentLabels.empty())
75 + {
76 + j["label"] = filters.presentLabels;
77 + }
78 +
79 + if (!filters.absentLabels.empty())
80 + {
81 + j["label!"] = filters.absentLabels;
82 + }
83 +
84 + return j;
85 +}
86 +
87 +} // namespace
88 +
89 +DockerHTTPClient::URL::URL(std::string&& Path) : m_path(std::move(Path))
90 +{
91 +}
92 +
93 +void DockerHTTPClient::URL::SetParameter(std::string&& Key, std::string&& Value)
94 +{
95 + m_parameters.emplace(std::move(Key), std::move(Value));
96 +}
97 +
98 +void DockerHTTPClient::URL::SetParameter(std::string&& Key, const std::string& Value)
99 +{
100 + m_parameters.emplace(std::move(Key), Value);
101 +}
102 +
103 +void DockerHTTPClient::URL::SetParameter(std::string&& Key, const char* Value)
104 +{
105 + SetParameter(std::move(Key), std::string(Value));
106 +}
107 +
108 +void DockerHTTPClient::URL::SetParameter(std::string&& Key, bool Value)
109 +{
110 + m_parameters.emplace(std::move(Key), Value ? "true" : "false");
111 +}
112 +
113 +std::string DockerHTTPClient::URL::Get() const
114 +{
115 + constexpr auto urlPrefix = "http://localhost";
116 +
117 + std::stringstream url;
118 + url << urlPrefix;
119 + url << m_path;
120 +
121 + if (!m_parameters.empty())
122 + {
123 + url << "?";
124 + bool first = true;
125 + for (const auto& [key, value] : m_parameters)
126 + {
127 + if (!first)
128 + {
129 + url << "&";
130 + }
131 +
132 + url << key << "=" << Escape(value);
133 + first = false;
134 + }
135 + }
136 +
137 + return url.str();
138 +}
139 +
140 +std::string DockerHTTPClient::URL::Escape(const std::string& Value)
141 +{
142 + auto escaped = winrt::Windows::Foundation::Uri::EscapeComponent(winrt::to_hstring(Value));
143 +
144 + return wsl::shared::string::WideToMultiByte(escaped.c_str());
145 +}
146 +
147 +DockerHTTPClient::DockerHTTPClient(wsl::shared::SocketChannel&& Channel, HANDLE exitingEvent, GUID VmId, ULONG ConnectTimeoutMs) :
148 + m_exitingEvent(exitingEvent), m_channel(std::move(Channel)), m_vmId(VmId), m_connectTimeoutMs(ConnectTimeoutMs)
149 +{
150 +}
151 +
152 +std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::PullImage(
153 + const std::string& Repo, const std::optional<std::string>& tagOrDigest, const std::optional<std::string>& registryAuth)
154 +{
155 + auto url = URL::Create("/images/create");
156 +
157 + // Normalize the repo server & path
158 + auto [server, path] = wslutil::NormalizeRepo(Repo);
159 + url.SetParameter("fromImage", std::format("{}/{}", server, path));
160 +
161 + if (tagOrDigest.has_value())
162 + {
163 + url.SetParameter("tag", tagOrDigest.value());
164 + }
165 +
166 + std::map<std::string, std::string> customHeaders;
167 +
168 + if (registryAuth.has_value())
169 + {
170 + customHeaders["X-Registry-Auth"] = registryAuth.value();
171 + }
172 +
173 + return SendRequestImpl(verb::post, url, {}, customHeaders);
174 +}
175 +
176 +std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::LoadImage(uint64_t ContentLength)
177 +{
178 + return SendRequestImpl(
179 + verb::post, URL::Create("/images/load"), {}, {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}});
180 +}
181 +
182 +std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength)
183 +{
184 + auto url = URL::Create("/images/create");
185 + url.SetParameter("tag", Tag);
186 + url.SetParameter("repo", Repo);
187 + url.SetParameter("fromSrc", "-");
188 +
189 + return SendRequestImpl(verb::post, url, {}, {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}});
190 +}
191 +
192 +void DockerHTTPClient::TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag)
193 +{
194 + auto url = URL::Create("/images/{}/tag", Id);
195 + url.SetParameter("repo", Repo);
196 + url.SetParameter("tag", Tag);
197 +
198 + Transaction<docker_schema::EmptyRequest>(verb::post, url);
199 +}
200 +
201 +std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::PushImage(
202 + const std::string& ImageName, const std::optional<std::string>& tag, const std::string& registryAuth)
203 +{
204 + auto url = URL::Create("/images/{}/push", ImageName);
205 +
206 + if (tag.has_value())
207 + {
208 + url.SetParameter("tag", tag.value());
209 + }
210 +
211 + std::map<std::string, std::string> customHeaders = {{"X-Registry-Auth", registryAuth}};
212 + return SendRequestImpl(verb::post, url, {}, customHeaders);
213 +}
214 +
215 +std::string DockerHTTPClient::Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password)
216 +{
217 + auto response = Transaction<docker_schema::AuthRequest>(
218 + verb::post, URL::Create("/auth"), {.username = username, .password = password, .serveraddress = serverAddress});
219 +
220 + return response.IdentityToken.value_or("");
221 +}
222 +
223 +std::vector<docker_schema::Image> DockerHTTPClient::ListImages(bool all, bool digests, const ListImagesFilters& filters)
224 +{
225 + auto url = URL::Create("/images/json");
226 +
227 + url.SetParameter("all", all);
228 + url.SetParameter("digests", digests);
229 +
230 + // Build filters JSON if any filters are set
231 + nlohmann::json filtersJson;
232 +
233 + if (filters.reference.has_value())
234 + {
235 + filtersJson["reference"] = nlohmann::json::array({filters.reference.value()});
236 + }
237 +
238 + if (filters.before.has_value())
239 + {
240 + filtersJson["before"] = nlohmann::json::array({filters.before.value()});
241 + }
242 +
243 + if (filters.since.has_value())
244 + {
245 + filtersJson["since"] = nlohmann::json::array({filters.since.value()});
246 + }
247 +
248 + if (filters.dangling.has_value())
249 + {
250 + filtersJson["dangling"] = nlohmann::json::array({filters.dangling.value() ? "true" : "false"});
251 + }
252 +
253 + if (!filters.labels.empty())
254 + {
255 + filtersJson["label"] = filters.labels;
256 + }
257 +
258 + if (!filtersJson.empty())
259 + {
260 + url.SetParameter("filters", filtersJson.dump());
261 + }
262 +
263 + return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::Image>>(verb::get, url);
264 +}
265 +
266 +docker_schema::InspectImage DockerHTTPClient::InspectImage(const std::string& NameOrId)
267 +{
268 + return Transaction<docker_schema::EmptyRequest, docker_schema::InspectImage>(verb::get, URL::Create("/images/{}/json", NameOrId));
269 +}
270 +
271 +std::vector<docker_schema::DeletedImage> wsl::windows::service::wslc::DockerHTTPClient::DeleteImage(const char* Image, bool Force, bool NoPrune)
272 +{
273 + auto url = URL::Create("/images/{}", Image);
274 + url.SetParameter("force", Force);
275 + url.SetParameter("noprune", NoPrune);
276 +
277 + return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::DeletedImage>>(verb::delete_, url);
278 +}
279 +
280 +std::pair<uint32_t, wil::unique_socket> DockerHTTPClient::SaveImage(const std::string& NameOrId)
281 +{
282 + auto [response, socket] = SendRequest(verb::get, URL::Create("/images/{}/get", NameOrId), {}, {});
283 +
284 + return {response.result_int(), std::move(socket)};
285 +}
286 +
287 +docker_schema::PruneImageResult DockerHTTPClient::PruneImages(const PruneImagesFilters& filters)
288 +{
289 + auto url = URL::Create("/images/prune");
290 +
291 + auto filtersJson = PruneFiltersToJson(filters);
292 + if (!filtersJson.empty())
293 + {
294 + url.SetParameter("filters", filtersJson.dump());
295 + }
296 +
297 + return Transaction<docker_schema::EmptyRequest, docker_schema::PruneImageResult>(verb::post, url);
298 +}
299 +
300 +std::vector<docker_schema::ContainerInfo> DockerHTTPClient::ListContainers(bool all)
301 +{
302 + auto url = URL::Create("/containers/json");
303 + url.SetParameter("all", all);
304 +
305 + return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::ContainerInfo>>(verb::get, url);
306 +}
307 +
308 +docker_schema::CreatedContainer DockerHTTPClient::CreateContainer(const docker_schema::CreateContainer& Request, const std::optional<std::string>& Name)
309 +{
310 + auto url = URL::Create("/containers/create");
311 + if (Name.has_value())
312 + {
313 + url.SetParameter("name", Name.value());
314 + }
315 +
316 + return Transaction<docker_schema::CreateContainer>(verb::post, url, Request);
317 +}
318 +
319 +void DockerHTTPClient::ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns)
320 +{
321 + auto url = URL::Create("/containers/{}/resize", Id);
322 + url.SetParameter("w", std::to_string(Columns));
323 + url.SetParameter("h", std::to_string(Rows));
324 +
325 + Transaction(verb::post, url);
326 +}
327 +
328 +void DockerHTTPClient::StartContainer(const std::string& Id, const std::optional<std::string>& DetachKeys)
329 +{
330 + auto url = URL::Create("/containers/{}/start", Id);
331 + if (DetachKeys.has_value())
332 + {
333 + url.SetParameter("detachKeys", DetachKeys.value());
334 + }
335 +
336 + Transaction(verb::post, url);
337 +}
338 +
339 +void DockerHTTPClient::StopContainer(const std::string& Id, std::optional<WSLCSignal> Signal, std::optional<ULONG> TimeoutSeconds)
340 +{
341 + auto url = URL::Create("/containers/{}/stop", Id);
342 + if (Signal.has_value())
343 + {
344 + url.SetParameter("signal", std::to_string(static_cast<int>(Signal.value())));
345 + }
346 +
347 + if (TimeoutSeconds.has_value())
348 + {
349 + url.SetParameter("t", std::to_string(TimeoutSeconds.value()));
350 + }
351 +
352 + Transaction(verb::post, url);
353 +}
354 +
355 +void DockerHTTPClient::SignalContainer(const std::string& Id, std::optional<WSLCSignal> Signal)
356 +{
357 + auto url = URL::Create("/containers/{}/kill", Id);
358 + if (Signal.has_value())
359 + {
360 + url.SetParameter("signal", std::to_string(static_cast<int>(Signal.value())));
361 + }
362 +
363 + Transaction(verb::post, url);
364 +}
365 +
366 +void DockerHTTPClient::DeleteContainer(const std::string& Id, bool Force, bool DeleteVolumes)
367 +{
368 + auto url = URL::Create("/containers/{}", Id);
369 +
370 + if (Force)
371 + {
372 + url.SetParameter("force", true);
373 + }
374 +
375 + if (DeleteVolumes)
376 + {
377 + url.SetParameter("v", true);
378 + }
379 +
380 + Transaction(verb::delete_, url);
381 +}
382 +
383 +docker_schema::InspectContainer DockerHTTPClient::InspectContainer(const std::string& Id)
384 +{
385 + return Transaction<EmptyRequest, docker_schema::InspectContainer>(verb::get, URL::Create("/containers/{}/json", Id));
386 +}
387 +
388 +docker_schema::InspectExec DockerHTTPClient::InspectExec(const std::string& Id)
389 +{
390 + return Transaction<EmptyRequest, docker_schema::InspectExec>(verb::get, URL::Create("/exec/{}/json", Id));
391 +}
392 +
393 +wil::unique_socket DockerHTTPClient::AttachContainer(const std::string& Id, const std::optional<std::string>& DetachKeys)
394 +{
395 + std::map<std::string, std::string> headers{{"Upgrade", "tcp"}, {"Connection", "upgrade"}};
396 +
397 + auto url = URL::Create("/containers/{}/attach", Id);
398 + url.SetParameter("stream", true);
399 + url.SetParameter("stdin", true);
400 + url.SetParameter("stdout", true);
401 + url.SetParameter("stderr", true);
402 +
403 + if (DetachKeys.has_value())
404 + {
405 + url.SetParameter("detachKeys", DetachKeys.value());
406 + }
407 +
408 + auto [response, socket] = SendRequest(verb::post, url, {}, headers);
409 +
410 + if (response.result_int() != 101)
411 + {
412 + throw DockerHTTPException(std::move(response), verb::post, url.Get(), "", "");
413 + }
414 +
415 + return std::move(socket);
416 +}
417 +
418 +std::pair<uint32_t, wil::unique_socket> DockerHTTPClient::ExportContainer(const std::string& ContainerNameOrId)
419 +{
420 + auto [response, socket] = SendRequest(verb::get, URL::Create("/containers/{}/export", ContainerNameOrId), {}, {});
421 +
422 + return {response.result_int(), std::move(socket)};
423 +}
424 +
425 +docker_schema::Volume DockerHTTPClient::CreateVolume(const docker_schema::CreateVolume& Request)
426 +{
427 + return Transaction<docker_schema::CreateVolume>(verb::post, URL::Create("/volumes/create"), Request);
428 +}
429 +
430 +void DockerHTTPClient::RemoveVolume(const std::string& Name)
431 +{
432 + Transaction(verb::delete_, URL::Create("/volumes/{}", Name));
433 +}
434 +
435 +std::vector<docker_schema::Volume> DockerHTTPClient::ListVolumes()
436 +{
437 + auto response = Transaction<docker_schema::EmptyRequest, docker_schema::ListVolumesResponse>(verb::get, URL::Create("/volumes"));
438 + return response.Volumes;
439 +}
440 +
441 +docker_schema::CreateNetworkResponse DockerHTTPClient::CreateNetwork(const docker_schema::CreateNetwork& Request)
442 +{
443 + return Transaction(verb::post, URL::Create("/networks/create"), Request);
444 +}
445 +
446 +void DockerHTTPClient::RemoveNetwork(const std::string& Name)
447 +{
448 + Transaction(verb::delete_, URL::Create("/networks/{}", Name));
449 +}
450 +
451 +std::vector<docker_schema::Network> DockerHTTPClient::ListNetworks()
452 +{
453 + return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::Network>>(verb::get, URL::Create("/networks"));
454 +}
455 +
456 +docker_schema::Network DockerHTTPClient::InspectNetwork(const std::string& Name)
457 +{
458 + return Transaction<docker_schema::EmptyRequest, docker_schema::Network>(verb::get, URL::Create("/networks/{}", Name));
459 +}
460 +
461 +wil::unique_socket DockerHTTPClient::ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail)
462 +{
463 + auto url = URL::Create("/containers/{}/logs", Id);
464 + url.SetParameter("follow", WI_IsFlagSet(Flags, WSLCLogsFlagsFollow));
465 + url.SetParameter("stdout", true);
466 + url.SetParameter("stderr", true);
467 + url.SetParameter("timestamps", WI_IsFlagSet(Flags, WSLCLogsFlagsTimestamps));
468 +
469 + if (Tail != 0)
470 + {
471 + url.SetParameter("tail", std::to_string(Tail));
472 + }
473 +
474 + if (Until != 0)
475 + {
476 + url.SetParameter("until", std::to_string(Until));
477 + }
478 +
479 + if (Since != 0)
480 + {
481 + url.SetParameter("since", std::to_string(Since));
482 + }
483 +
484 + auto [response, socket] = SendRequest(verb::get, url, {}, {});
485 + if (response.result_int() != 200)
486 + {
487 + throw DockerHTTPException(std::move(response), verb::get, url.Get(), "", "");
488 + }
489 +
490 + return std::move(socket);
491 +}
492 +
493 +docker_schema::PruneContainerResult DockerHTTPClient::PruneContainers(const PruneContainersFilters& filters)
494 +{
495 + auto url = URL::Create("/containers/prune");
496 +
497 + auto filtersJson = PruneFiltersToJson(filters);
498 + if (!filtersJson.empty())
499 + {
500 + url.SetParameter("filters", filtersJson.dump());
501 + }
502 +
503 + return Transaction<docker_schema::EmptyRequest, docker_schema::PruneContainerResult>(verb::post, url);
504 +}
505 +
506 +docker_schema::CreateExecResponse DockerHTTPClient::CreateExec(const std::string& Container, const docker_schema::CreateExec& Request)
507 +{
508 + return Transaction<docker_schema::CreateExec>(verb::post, URL::Create("/containers/{}/exec", Container), Request);
509 +}
510 +
511 +wil::unique_socket DockerHTTPClient::StartExec(const std::string& Id, const common::docker_schema::StartExec& Request)
512 +{
513 + std::map<std::string, std::string> headers{{"Upgrade", "tcp"}, {"Connection", "upgrade"}};
514 +
515 + auto url = URL::Create("/exec/{}/start", Id);
516 +
517 + auto body = wsl::shared::ToJson(Request);
518 + auto [response, socket] = SendRequest(verb::post, url, body, headers);
519 + if (response.result_int() != 101)
520 + {
521 + throw DockerHTTPException(std::move(response), verb::post, url.Get(), std::move(body), "");
522 + }
523 + return std::move(socket);
524 +}
525 +
526 +void DockerHTTPClient::ResizeExecTty(const std::string& Id, ULONG Rows, ULONG Columns)
527 +{
528 + auto url = URL::Create("/exec/{}/resize", Id);
529 + url.SetParameter("w", std::to_string(Columns));
530 + url.SetParameter("h", std::to_string(Rows));
531 +
532 + Transaction(verb::post, url);
533 +}
534 +
535 +wil::unique_socket DockerHTTPClient::MonitorEvents()
536 +{
537 + auto url = URL::Create("/events");
538 + auto [response, socket] = SendRequest(verb::get, url, {});
539 +
540 + if (response.result_int() != 200)
541 + {
542 + throw DockerHTTPException(std::move(response), verb::get, url.Get(), "", "");
543 + }
544 +
545 + return std::move(socket);
546 +}
547 +
548 +wil::unique_socket DockerHTTPClient::ConnectSocket()
549 +{
550 + auto lock = m_lock.lock_exclusive();
551 +
552 + // Send a fork message.
553 + WSLC_FORK message;
554 + message.ForkType = WSLC_FORK::Thread;
555 + const auto& response = m_channel.Transaction(message);
556 +
557 + THROW_HR_IF_MSG(E_FAIL, response.Pid <= 0, "fork() returned %i", response.Pid);
558 +
559 + // Connect the new hvsocket.
560 + wsl::shared::SocketChannel newChannel{
561 + wsl::windows::common::hvsocket::Connect(m_vmId, response.Port, m_exitingEvent, m_connectTimeoutMs), "DockerClient", m_exitingEvent};
562 + lock.reset();
563 +
564 + // Connect that socket to the docker unix socket.
565 + shared::MessageWriter<WSLC_UNIX_CONNECT> writer;
566 + writer.WriteString(writer->PathOffset, "/var/run/docker.sock");
567 +
568 + auto result = newChannel.Transaction<WSLC_UNIX_CONNECT>(writer.Span());
569 + THROW_HR_IF_MSG(E_FAIL, result.Result < 0, "Failed to connect to unix socket: '/var/run/docker.sock', %i", result.Result);
570 +
571 + return newChannel.Release();
572 +}
573 +
574 +std::pair<DockerHTTPClient::HTTPResponse, std::string> DockerHTTPClient::SendRequestAndReadResponse(verb Method, const URL& Url, const std::string& Body)
575 +{
576 + // Send the request.
577 + auto context = SendRequestImpl(Method, Url, Body, {});
578 +
579 + // Read the response header and body.
580 + // Limit response size to prevent unbounded memory growth from pathological responses.
581 + // All callers expect JSON metadata (list, inspect, create, etc.), not large binary payloads.
582 + constexpr size_t MaxResponseSize = 64 * _1MB;
583 +
584 + std::optional<HTTPResponse> responseHeader;
585 + std::string responseBody;
586 + const auto& url = Url;
587 + auto OnResponse = [&responseBody, &url](const gsl::span<char>& span) {
588 + THROW_HR_IF_MSG(
589 + HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE),
590 + span.size() > MaxResponseSize - responseBody.size(),
591 + "Docker API response exceeds maximum size (%zu bytes) for %hs",
592 + MaxResponseSize,
593 + url.Get().c_str());
594 + responseBody.append(span.data(), span.size());
595 + };
596 +
597 + auto onHttpResponse = [&](const auto& response) { responseHeader = response; };
598 + MultiHandleWait io;
599 +
600 + io.AddHandle(std::make_unique<relay::EventHandle>(m_exitingEvent, [&]() { THROW_HR(E_ABORT); }));
601 + io.AddHandle(std::make_unique<DockerHttpResponseHandle>(*context, std::move(onHttpResponse), std::move(OnResponse)), MultiHandleWait::CancelOnCompleted);
602 +
603 + io.Run({});
604 +
605 + THROW_HR_IF(E_UNEXPECTED, !responseHeader.has_value());
606 +
607 + return {std::move(responseHeader.value()), responseBody};
608 +}
609 +
610 +DockerHTTPClient::DockerHttpResponseHandle::DockerHttpResponseHandle(
611 + HTTPRequestContext& context,
612 + std::function<void(const HTTPResponse&)>&& onResponseHeader,
613 + std::function<void(const gsl::span<char>&)>&& onResponseBytes,
614 + std::function<void()>&& onCompleted) :
615 + common::relay::ReadHandle(
616 + HandleWrapper{context.stream.native_handle()}, std::bind(&DockerHttpResponseHandle::OnRead, this, std::placeholders::_1)),
617 + Context(context),
618 + OnResponseHeader(std::move(onResponseHeader)),
619 + OnResponse(std::move(onResponseBytes)),
620 + OnCompleted(std::move(onCompleted))
621 +{
622 +}
623 +
624 +DockerHTTPClient::DockerHttpResponseHandle::~DockerHttpResponseHandle()
625 +{
626 + if (State == common::relay::IOHandleStatus::Completed)
627 + {
628 + OnCompleted();
629 + }
630 +}
631 +
632 +void DockerHTTPClient::DockerHttpResponseHandle::OnRead(const gsl::span<char>& Content)
633 +{
634 + // If the HTTP parser is done, then these bytes are part of the response body
635 + if (Parser.is_header_done())
636 + {
637 + OnResponseBytes(Content);
638 + }
639 + else
640 + {
641 + // Otherwise keep parsing the HTTP response header.
642 + size_t i{};
643 + for (i = 0; i < Content.size() && LineFeeds < 2; i++)
644 + {
645 + if (Content[i] == '\n')
646 + {
647 + LineFeeds++;
648 + }
649 + else if (Content[i] != '\r')
650 + {
651 + LineFeeds = 0;
652 + }
653 + }
654 +
655 + // Feed the parser up to the end of the header.
656 + boost::beast::error_code error;
657 + Parser.put(boost::asio::buffer(Content.data(), i), error);
658 +
659 + THROW_HR_IF_MSG(
660 + E_UNEXPECTED, error && error != boost::beast::http::error::need_more, "Error parsing HTTP response: %hs", error.what().c_str());
661 +
662 + if (Parser.is_header_done())
663 + {
664 + const auto& response = Parser.get();
665 + OnResponseHeader(response);
666 +
667 + // If the response is chunked, then create a chunked reader.
668 + if (IsResponseChunked(response))
669 + {
670 + ResponseParser.emplace(HandleWrapper{Context.stream.native_handle()}, std::move(OnResponse));
671 + }
672 +
673 + auto contentLength = response.find(http::field::content_length);
674 + if (contentLength != response.end())
675 + {
676 + try
677 + {
678 + RemainingContentLength = std::stoull(contentLength->value());
679 + }
680 + catch (const std::exception&)
681 + {
682 + THROW_HR_MSG(
683 + E_UNEXPECTED,
684 + "Invalid Content-Length header: %.*hs",
685 + static_cast<int>(contentLength->value().size()),
686 + contentLength->value().data());
687 + }
688 + }
689 + }
690 +
691 + // If any buffer remains, then it's part of the response body.
692 + auto remaining = Content.subspan(i);
693 + if (!remaining.empty())
694 + {
695 + WI_ASSERT(Parser.is_header_done());
696 + OnResponseBytes(remaining);
697 + }
698 + }
699 +}
700 +
701 +void DockerHTTPClient::DockerHttpResponseHandle::OnResponseBytes(const gsl::span<char>& Content)
702 +{
703 + auto span = Content;
704 +
705 + // If the HTTP response had a Content-Length, make sure not to read past it.
706 + if (RemainingContentLength.has_value())
707 + {
708 + auto consume = std::min(span.size(), RemainingContentLength.value());
709 +
710 + *RemainingContentLength -= consume;
711 + if (*RemainingContentLength == 0)
712 + {
713 + State = common::relay::IOHandleStatus::Completed;
714 + }
715 +
716 + span = span.subspan(0, consume);
717 + }
718 +
719 + if (ResponseParser.has_value())
720 + {
721 + ResponseParser->OnRead(span);
722 + }
723 + else
724 + {
725 + OnResponse(span);
726 + }
727 +}
728 +
729 +std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::SendRequestImpl(
730 + verb Method, const URL& Url, const std::string& Body, const std::map<std::string, std::string>& Headers)
731 +{
732 + auto context = std::make_unique<DockerHTTPClient::HTTPRequestContext>(ConnectSocket());
733 +
734 + http::request<http::string_body> req{Method, Url.Get(), 11};
735 + if (!Body.empty())
736 + {
737 + req.set(http::field::content_type, "application/json");
738 + req.body() = Body;
739 +
740 + // N.B. prepare_payload() overrides content-length.
741 + req.prepare_payload();
742 + }
743 +
744 + req.set(http::field::host, "localhost");
745 + req.set(http::field::connection, "close");
746 + req.set(http::field::accept, "application/json");
747 +
748 + for (const auto& [name, value] : Headers)
749 + {
750 + req.set(name, value);
751 + }
752 +
753 + http::write(context->stream, req);
754 +
755 +#ifdef WSLC_HTTP_DEBUG
756 +
757 + std::ostringstream oss;
758 + oss << req;
759 +
760 + auto requestString = oss.str();
761 +
762 + WSL_LOG("HTTPRequestDebug", TraceLoggingValue(Url.Get().c_str(), "Url"), TraceLoggingValue(requestString.c_str(), "Request"));
763 +
764 +#endif
765 +
766 + return std::move(context);
767 +}
768 +
769 +std::pair<DockerHTTPClient::HTTPResponse, wil::unique_socket> DockerHTTPClient::SendRequest(
770 + verb Method, const URL& Url, const std::string& Body, const std::map<std::string, std::string>& Headers)
771 +{
772 + // Write the request
773 + auto context = SendRequestImpl(Method, Url, Body, Headers);
774 +
775 + // Parse the response header
776 + constexpr auto bufferSize = 16 * 1024;
777 + size_t Offset = 0;
778 + std::vector<char> buffer;
779 + http::response_parser<http::buffer_body> parser;
780 + parser.eager(false);
781 + parser.skip(false);
782 +
783 + size_t lineFeeds = 0;
784 + // Consume the socket until the header end is reached
785 + while (!parser.is_header_done())
786 + {
787 + buffer.resize(Offset + bufferSize);
788 +
789 + // Peek for the end of the HTTP header '\r\n'
790 + auto bytesRead = common::socket::Receive(
791 + context->stream.native_handle(), gsl::span(reinterpret_cast<gsl::byte*>(buffer.data() + Offset), bufferSize), m_exitingEvent, MSG_PEEK);
792 +
793 + THROW_HR_IF(E_ABORT, bytesRead == 0);
794 +
795 + size_t i{};
796 + for (i = 0; i < bytesRead + Offset && lineFeeds < 2; i++)
797 + {
798 + if (buffer[i] == '\n')
799 + {
800 + lineFeeds++;
801 + }
802 + else if (buffer[i] != '\r')
803 + {
804 + lineFeeds = 0;
805 + }
806 + }
807 +
808 + // Consume the buffer from the socket.
809 + bytesRead = common::socket::Receive(
810 + context->stream.native_handle(), gsl::span(reinterpret_cast<gsl::byte*>(buffer.data() + Offset), i - Offset), m_exitingEvent);
811 + WI_ASSERT(bytesRead == i - Offset);
812 +
813 + Offset += bytesRead;
814 + buffer.resize(Offset);
815 +
816 + if (lineFeeds == 2) // Header is complete, feed it to the parser.
817 + {
818 +
819 +#ifdef WSLC_HTTP_DEBUG
820 +
821 + buffer.push_back('\0');
822 + WSL_LOG(
823 + "HTTPResponseDebug", TraceLoggingValue(Url.Get().c_str(), "Url"), TraceLoggingValue(buffer.data(), "Response"));
824 + buffer.pop_back();
825 +
826 +#endif
827 +
828 + boost::beast::error_code error;
829 + parser.put(boost::asio::buffer(buffer.data(), buffer.size()), error);
830 +
831 + THROW_HR_IF_MSG(
832 + E_UNEXPECTED,
833 + error && error != boost::beast::http::error::need_more,
834 + "Error parsing HTTP response: %hs",
835 + error.what().c_str());
836 + }
837 + }
838 +
839 + return {parser.get(), wil::unique_socket{context->stream.release()}};
840 +}
src/windows/wslcsession/DockerHTTPClient.h new
+293
@@ -0,0 +1,293 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + DockerHTTPClient.h
8 +
9 +Abstract:
10 +
11 + This file contains the definition of the Docker HTTP client.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include <boost/asio.hpp>
18 +#include <boost/asio/generic/stream_protocol.hpp>
19 +#include <boost/beast/core.hpp>
20 +#include <boost/beast/http.hpp>
21 +#include "relay.hpp"
22 +#include "docker_schema.h"
23 +
24 +#define THROW_DOCKER_USER_ERROR_MSG(_Ex, _Msg, ...) \
25 + if ((_Ex).HasErrorMessage()) \
26 + { \
27 + THROW_HR_WITH_USER_ERROR_MSG( \
28 + (_Ex).HResultFromStatusCode(), (_Ex).DockerMessage<wsl::windows::common::docker_schema::ErrorResponse>().message, _Msg, ##__VA_ARGS__); \
29 + } \
30 + else \
31 + { \
32 + THROW_HR_MSG((_Ex).HResultFromStatusCode(), "Error: %hs. " _Msg, (_Ex).what(), ##__VA_ARGS__); \
33 + }
34 +
35 +#define CATCH_AND_THROW_DOCKER_USER_ERROR(_Msg, ...) \
36 + catch (const DockerHTTPException& e) \
37 + { \
38 + THROW_DOCKER_USER_ERROR_MSG(e, _Msg, ##__VA_ARGS__) \
39 + }
40 +
41 +namespace wsl::windows::service::wslc {
42 +
43 +class DockerHTTPException : public std::runtime_error
44 +{
45 +public:
46 + DockerHTTPException(
47 + boost::beast::http::message<false, boost::beast::http::buffer_body>&& Response,
48 + boost::beast::http::verb Method,
49 + std::string&& Url,
50 + std::string&& RequestContent,
51 + std::string&& ResponseContent) :
52 + std::runtime_error(std::format(
53 + "HTTP request failed: {} {} -> {} (Request: {}, Response: {})", boost::beast::http::to_string(Method), Url, Response.result_int(), RequestContent, ResponseContent)),
54 + m_response(std::move(Response)),
55 + m_url(std::move(Url)),
56 + m_request(std::move(RequestContent)),
57 + m_responseBody(std::move(ResponseContent))
58 + {
59 + }
60 +
61 + template <typename T = docker_schema::ErrorResponse>
62 + T DockerMessage() const
63 + {
64 + return wsl::shared::FromJson<T>(m_responseBody.c_str());
65 + }
66 +
67 + // Only try to decode the error message if it's actually json.
68 + bool HasErrorMessage() const
69 + {
70 + auto it = m_response.find(boost::beast::http::field::content_type);
71 + return it != m_response.end() && it->value().starts_with("application/json");
72 + }
73 +
74 + uint16_t StatusCode() const noexcept
75 + {
76 + return static_cast<uint16_t>(m_response.result());
77 + }
78 +
79 + HRESULT HResultFromStatusCode() const noexcept
80 + {
81 + if (StatusCode() == 400)
82 + {
83 + return E_INVALIDARG;
84 + }
85 + else
86 + {
87 + return E_FAIL;
88 + }
89 + }
90 +
91 +private:
92 + boost::beast::http::message<false, boost::beast::http::buffer_body> m_response{};
93 + std::string m_url;
94 + std::string m_request;
95 + std::string m_responseBody;
96 +};
97 +
98 +class DockerHTTPClient
99 +{
100 + NON_COPYABLE(DockerHTTPClient);
101 +
102 +public:
103 + using OnResponseBytes = std::function<void(gsl::span<char>)>;
104 +
105 + struct HTTPRequestContext
106 + {
107 + NON_COPYABLE(HTTPRequestContext);
108 + NON_MOVABLE(HTTPRequestContext);
109 +
110 + HTTPRequestContext(wil::unique_socket&& Socket) : stream(context)
111 + {
112 + boost::asio::generic::stream_protocol hv_proto(AF_HYPERV, SOCK_STREAM);
113 + stream.assign(hv_proto, Socket.release());
114 + }
115 +
116 + boost::asio::io_context context;
117 + boost::asio::generic::stream_protocol::socket stream;
118 + };
119 +
120 + using HTTPResponse = boost::beast::http::message<false, boost::beast::http::buffer_body>;
121 +
122 + DockerHTTPClient(wsl::shared::SocketChannel&& Channel, HANDLE ExitingEvent, GUID VmId, ULONG ConnectTimeoutMs);
123 +
124 + // Container management.
125 + struct PruneContainersFilters
126 + {
127 + std::optional<std::uint64_t> until;
128 + std::vector<std::string> presentLabels;
129 + std::vector<std::string> absentLabels;
130 + };
131 +
132 + std::vector<common::docker_schema::ContainerInfo> ListContainers(bool all = false);
133 + common::docker_schema::CreatedContainer CreateContainer(const common::docker_schema::CreateContainer& Request, const std::optional<std::string>& Name);
134 + void StartContainer(const std::string& Id, const std::optional<std::string>& DetachKeys);
135 + void StopContainer(const std::string& Id, std::optional<WSLCSignal> Signal, std::optional<ULONG> TimeoutSeconds);
136 + void DeleteContainer(const std::string& Id, bool Force, bool DeleteVolumes = false);
137 + void SignalContainer(const std::string& Id, std::optional<WSLCSignal> Signal);
138 + common::docker_schema::InspectContainer InspectContainer(const std::string& Id);
139 + common::docker_schema::InspectExec InspectExec(const std::string& Id);
140 + wil::unique_socket AttachContainer(const std::string& Id, const std::optional<std::string>& DetachKeys);
141 + void ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns);
142 + wil::unique_socket ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail);
143 + std::pair<uint32_t, wil::unique_socket> ExportContainer(const std::string& ContainerID);
144 + common::docker_schema::PruneContainerResult PruneContainers(const PruneContainersFilters& filters = {});
145 +
146 + // Volume management.
147 + common::docker_schema::Volume CreateVolume(const common::docker_schema::CreateVolume& Request);
148 + void RemoveVolume(const std::string& Name);
149 + std::vector<common::docker_schema::Volume> ListVolumes();
150 +
151 + // Network management.
152 + common::docker_schema::CreateNetworkResponse CreateNetwork(const common::docker_schema::CreateNetwork& Request);
153 + void RemoveNetwork(const std::string& Name);
154 + std::vector<common::docker_schema::Network> ListNetworks();
155 + common::docker_schema::Network InspectNetwork(const std::string& Name);
156 +
157 + // Image management.
158 + struct ListImagesFilters
159 + {
160 + std::optional<std::string> reference;
161 + std::optional<std::string> before;
162 + std::optional<std::string> since;
163 + std::optional<bool> dangling;
164 + std::vector<std::string> labels;
165 + };
166 +
167 + struct PruneImagesFilters
168 + {
169 + std::optional<bool> dangling;
170 + std::optional<std::uint64_t> until;
171 + std::vector<std::string> presentLabels;
172 + std::vector<std::string> absentLabels;
173 + };
174 +
175 + std::unique_ptr<HTTPRequestContext> PullImage(
176 + const std::string& Repo, const std::optional<std::string>& tagOrDigest, const std::optional<std::string>& registryAuth = std::nullopt);
177 + std::unique_ptr<HTTPRequestContext> ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength);
178 + std::unique_ptr<HTTPRequestContext> LoadImage(uint64_t ContentLength);
179 + void TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag);
180 + std::unique_ptr<HTTPRequestContext> PushImage(const std::string& ImageName, const std::optional<std::string>& tag, const std::string& registryAuth);
181 + std::string Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password);
182 + std::vector<common::docker_schema::Image> ListImages(bool all = false, bool digests = false, const ListImagesFilters& filters = {});
183 + common::docker_schema::InspectImage InspectImage(const std::string& NameOrId);
184 + std::vector<common::docker_schema::DeletedImage> DeleteImage(const char* Image, bool Force, bool NoPrune); // Image can be ID or Repo:Tag.
185 + std::pair<uint32_t, wil::unique_socket> SaveImage(const std::string& NameOrId);
186 + common::docker_schema::PruneImageResult PruneImages(const PruneImagesFilters& filters = {});
187 +
188 + // Exec.
189 + common::docker_schema::CreateExecResponse CreateExec(const std::string& Container, const common::docker_schema::CreateExec& Request);
190 + wil::unique_socket StartExec(const std::string& Id, const common::docker_schema::StartExec& Request);
191 + void ResizeExecTty(const std::string& Id, ULONG Rows, ULONG Columns);
192 +
193 + wil::unique_socket MonitorEvents();
194 +
195 + struct DockerHttpResponseHandle : public common::relay::ReadHandle
196 + {
197 + NON_COPYABLE(DockerHttpResponseHandle);
198 + NON_MOVABLE(DockerHttpResponseHandle);
199 +
200 + DockerHttpResponseHandle(
201 + HTTPRequestContext& context,
202 + std::function<void(const HTTPResponse&)>&& OnResponseHeader,
203 + std::function<void(const gsl::span<char>&)>&& OnResponseBytes,
204 + std::function<void()>&& OnCompleted = []() {});
205 +
206 + ~DockerHttpResponseHandle();
207 +
208 + private:
209 + void OnRead(const gsl::span<char>& Content);
210 + void OnResponseBytes(const gsl::span<char>& Content);
211 +
212 + HTTPRequestContext& Context;
213 + std::function<void(const boost::beast::http::message<false, boost::beast::http::buffer_body>&)> OnResponseHeader;
214 + std::function<void(const gsl::span<char>&)> OnResponse;
215 + std::function<void()> OnCompleted;
216 + boost::beast::http::response_parser<boost::beast::http::buffer_body> Parser;
217 + size_t LineFeeds = 0;
218 + std::optional<size_t> RemainingContentLength;
219 + std::optional<common::relay::HTTPChunkBasedReadHandle> ResponseParser;
220 + };
221 +
222 +private:
223 + class URL
224 + {
225 + public:
226 + std::string Get() const;
227 + void SetParameter(std::string&& Key, std::string&& Value);
228 + void SetParameter(std::string&& Key, const std::string& Value);
229 + void SetParameter(std::string&& Key, const char* Value); // Overload so that pointers don't resolve to the bool method.
230 + void SetParameter(std::string&& Key, bool Value);
231 +
232 + template <typename... Args>
233 + static auto Create(std::format_string<decltype(URL::Escape(std::declval<Args>()))...> Url, Args&&... args)
234 + {
235 + WI_ASSERT(Url.get().find_first_of("?!") == std::string::npos);
236 +
237 + return URL(std::format(Url, Escape(std::forward<Args>(args))...));
238 + }
239 +
240 + private:
241 + URL(std::string&& Path);
242 +
243 + static std::string Escape(const std::string& Value);
244 +
245 + std::string m_path;
246 + std::map<std::string, std::string> m_parameters;
247 + };
248 +
249 + wil::unique_socket ConnectSocket();
250 +
251 + std::unique_ptr<HTTPRequestContext> SendRequestImpl(
252 + boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map<std::string, std::string>& Headers = {});
253 +
254 + std::pair<HTTPResponse, std::string> SendRequestAndReadResponse(
255 + boost::beast::http::verb Method, const URL& Url, const std::string& Body = "");
256 +
257 + std::pair<HTTPResponse, wil::unique_socket> SendRequest(
258 + boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map<std::string, std::string>& Headers = {});
259 +
260 + template <typename TRequest = common::docker_schema::EmptyRequest, typename TResponse = TRequest::TResponse>
261 + auto Transaction(boost::beast::http::verb Method, const URL& Url, const TRequest& RequestObject = {})
262 + {
263 + std::string requestString;
264 + if constexpr (!std::is_same_v<TRequest, common::docker_schema::EmptyRequest>)
265 + {
266 + requestString = wsl::shared::ToJson(RequestObject);
267 + }
268 +
269 + auto [response, body] = SendRequestAndReadResponse(Method, Url, requestString);
270 +
271 + WSL_LOG(
272 + "HTTPTransaction",
273 + TraceLoggingValue(Url.Get().c_str(), "URL"),
274 + TraceLoggingValue(response.result_int(), "StatusCode"));
275 +
276 + if (response.result_int() < 200 || response.result_int() >= 300)
277 + {
278 + throw DockerHTTPException(std::move(response), Method, Url.Get(), std::move(requestString), std::move(body));
279 + }
280 +
281 + if constexpr (!std::is_same_v<TResponse, void>)
282 + {
283 + return wsl::shared::FromJson<TResponse>(body.c_str());
284 + }
285 + }
286 +
287 + ULONG m_connectTimeoutMs{};
288 + GUID m_vmId;
289 + shared::SocketChannel m_channel;
290 + HANDLE m_exitingEvent;
291 + wil::srwlock m_lock;
292 +};
293 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/IORelay.cpp new
+97
@@ -0,0 +1,97 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + IORelay.cpp
8 +
9 +Abstract:
10 +
11 + Contains the implementation of the IORelay class.
12 +
13 +--*/
14 +
15 +#include "IORelay.h"
16 +
17 +using wsl::windows::common::relay::DockerIORelayHandle;
18 +using wsl::windows::common::relay::MultiHandleWait;
19 +using wsl::windows::common::relay::OverlappedIOHandle;
20 +using wsl::windows::service::wslc::IORelay;
21 +
22 +IORelay::IORelay()
23 +{
24 + m_thread = std::thread([this]() { Run(); });
25 +}
26 +
27 +IORelay::~IORelay()
28 +{
29 + Stop();
30 +}
31 +
32 +void IORelay::AddHandle(std::unique_ptr<common::relay::OverlappedIOHandle>&& Handle)
33 +{
34 + std::vector<std::unique_ptr<common::relay::OverlappedIOHandle>> handles;
35 + handles.emplace_back(std::move(Handle));
36 +
37 + AddHandles(std::move(handles));
38 +}
39 +
40 +void IORelay::AddHandles(std::vector<std::unique_ptr<common::relay::OverlappedIOHandle>>&& Handles)
41 +{
42 + WI_ASSERT(!m_exit);
43 +
44 + std::lock_guard lock(m_pendingHandlesLock);
45 +
46 + // Append the new handles
47 + // N.B. IgnoreErrors is set so the IO doesn't stop on individual handle errors.
48 +
49 + for (auto& e : Handles)
50 + {
51 + WI_ASSERT(!!e);
52 + m_pendingHandles.emplace_back(std::move(e));
53 + }
54 +
55 + // Restart the relay thread.
56 + m_refreshEvent.SetEvent();
57 +}
58 +
59 +void IORelay::Stop()
60 +{
61 + m_exit = true;
62 + m_refreshEvent.SetEvent();
63 +
64 + // Skip join if called from the IORelay thread itself (e.g., from a handle callback).
65 + if (m_thread.joinable() && m_thread.get_id() != std::this_thread::get_id())
66 + {
67 + m_thread.join();
68 + }
69 +}
70 +
71 +void IORelay::Run()
72 +try
73 +{
74 + common::wslutil::SetThreadDescription(L"IORelay");
75 +
76 + windows::common::relay::MultiHandleWait io;
77 +
78 + // N.B. All the IO must happen on the thread.
79 + // If the thread that scheduled the IO exits, the IO is cancelled.
80 + while (!m_exit)
81 + {
82 + {
83 + // Add any pending handles.
84 + std::lock_guard lock(m_pendingHandlesLock);
85 + for (auto& e : m_pendingHandles)
86 + {
87 + io.AddHandle(std::move(e), MultiHandleWait::IgnoreErrors);
88 + }
89 +
90 + m_pendingHandles.clear();
91 + }
92 +
93 + io.AddHandle(std::make_unique<common::relay::EventHandle>(m_refreshEvent.get()), MultiHandleWait::CancelOnCompleted);
94 + io.Run({});
95 + }
96 +}
97 +CATCH_LOG();
\ No newline at end of file
src/windows/wslcsession/IORelay.h new
+45
@@ -0,0 +1,45 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + IORelay.h
8 +
9 +Abstract:
10 +
11 + Contains the definition of the IORelay class.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +namespace wsl::windows::service::wslc {
18 +
19 +class IORelay
20 +{
21 +
22 +public:
23 + NON_COPYABLE(IORelay);
24 +
25 + IORelay();
26 + ~IORelay();
27 +
28 + void AddHandles(std::vector<std::unique_ptr<common::relay::OverlappedIOHandle>>&& Handles);
29 + void AddHandle(std::unique_ptr<common::relay::OverlappedIOHandle>&& Handle);
30 +
31 + void Stop();
32 +
33 +private:
34 + void Start();
35 + void Run();
36 +
37 + std::mutex m_pendingHandlesLock;
38 + wil::unique_event m_refreshEvent{wil::EventOptions::None};
39 + std::vector<std::unique_ptr<common::relay::OverlappedIOHandle>> m_pendingHandles;
40 +
41 + std::thread m_thread;
42 + std::atomic<bool> m_exit = false;
43 +};
44 +
45 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/IWSLCVolume.h new
+50
@@ -0,0 +1,50 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + IWSLCVolume.h
8 +
9 +Abstract:
10 +
11 + Abstract interface implemented by all WSLC-managed volume drivers
12 + (currently WSLCVhdVolumeImpl and WSLCGuestVolumeImpl). WSLCSession
13 + stores volumes through this interface so it can hold a single map
14 + regardless of the concrete driver type.
15 +
16 +--*/
17 +
18 +#pragma once
19 +
20 +#include "wslc.h"
21 +#include <string>
22 +
23 +namespace wsl::windows::service::wslc {
24 +
25 +class IWSLCVolume
26 +{
27 +public:
28 + virtual ~IWSLCVolume() = default;
29 +
30 + // The docker volume name.
31 + virtual const std::string& Name() const noexcept = 0;
32 +
33 + // The WSLC volume driver, e.g. "vhd" or "guest". This is the driver
34 + // stored in the WSLC volume metadata label, not the underlying docker
35 + // driver (which may be "local" for guest volumes).
36 + virtual const char* Driver() const noexcept = 0;
37 +
38 + // Remove the volume from docker and release any host-side resources
39 + // (e.g. detach/delete the VHD for VHD volumes). Throws on failure.
40 + virtual void Delete() = 0;
41 +
42 + // Returns a JSON string for the COM-facing InspectVolume result.
43 + virtual std::string Inspect() const = 0;
44 +
45 + // Returns the WSLCVolumeInformation struct for the COM-facing
46 + // ListVolumes / CreateVolume results.
47 + virtual WSLCVolumeInformation GetVolumeInformation() const = 0;
48 +};
49 +
50 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/PortRelayHandle.cpp new
+135
@@ -0,0 +1,135 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + PortRelayHandle.cpp
8 +
9 +Abstract:
10 +
11 + Contains the implementation of the PortRelayAcceptHandle class.
12 +
13 +--*/
14 +
15 +#include "PortRelayHandle.h"
16 +#include "IORelay.h"
17 +#include "hvsocket.hpp"
18 +#include "socket.hpp"
19 +#include "wslutil.h"
20 +#include "lxinitshared.h"
21 +#include <gslhelpers.h>
22 +#include <mswsock.h>
23 +#include <thread>
24 +
25 +using namespace wsl::windows::service::wslc;
26 +using namespace wsl::windows::common;
27 +
28 +PortRelayAcceptHandle::PortRelayAcceptHandle(
29 + wil::unique_socket&& ListenSocket, const GUID& VmId, uint32_t RelayPort, uint32_t LinuxPort, int Family, IORelay& IoRelay) :
30 + ListenSocket(std::move(ListenSocket)), VmId(VmId), RelayPort(RelayPort), LinuxPort(LinuxPort), Family(Family), IoRelay(IoRelay)
31 +{
32 + Overlapped.hEvent = Event.get();
33 +}
34 +
35 +PortRelayAcceptHandle::~PortRelayAcceptHandle()
36 +{
37 + if (State == relay::IOHandleStatus::Pending)
38 + {
39 + LOG_IF_WIN32_BOOL_FALSE(CancelIoEx(reinterpret_cast<HANDLE>(ListenSocket.get()), &Overlapped));
40 +
41 + DWORD bytesProcessed{};
42 + DWORD flagsReturned{};
43 + if (!WSAGetOverlappedResult(ListenSocket.get(), &Overlapped, &bytesProcessed, TRUE, &flagsReturned))
44 + {
45 + auto error = GetLastError();
46 + LOG_LAST_ERROR_IF(error != ERROR_CONNECTION_ABORTED && error != ERROR_OPERATION_ABORTED);
47 + }
48 + }
49 +}
50 +
51 +void PortRelayAcceptHandle::Schedule()
52 +{
53 + WI_ASSERT(State == relay::IOHandleStatus::Standby);
54 +
55 + // Create a new socket for accepting
56 + AcceptedSocket.reset(WSASocket(Family, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_OVERLAPPED));
57 + THROW_LAST_ERROR_IF(!AcceptedSocket);
58 +
59 + memset(AcceptBuffer, 0, sizeof(AcceptBuffer));
60 + DWORD bytesReturned{};
61 + if (AcceptEx(ListenSocket.get(), AcceptedSocket.get(), AcceptBuffer, 0, sizeof(SOCKADDR_STORAGE), sizeof(SOCKADDR_STORAGE), &bytesReturned, &Overlapped))
62 + {
63 + // Accept completed immediately
64 + State = relay::IOHandleStatus::Completed;
65 + }
66 + else
67 + {
68 + auto error = WSAGetLastError();
69 + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(error), error != ERROR_IO_PENDING, "Handle: 0x%p", reinterpret_cast<void*>(ListenSocket.get()));
70 +
71 + State = relay::IOHandleStatus::Pending;
72 + }
73 +}
74 +
75 +void PortRelayAcceptHandle::Collect()
76 +{
77 + WI_ASSERT(State == relay::IOHandleStatus::Pending || State == relay::IOHandleStatus::Completed);
78 +
79 + if (State == relay::IOHandleStatus::Pending)
80 + {
81 + DWORD bytesReceived{};
82 + DWORD flagsReturned{};
83 + THROW_IF_WIN32_BOOL_FALSE(WSAGetOverlappedResult(ListenSocket.get(), &Overlapped, &bytesReceived, false, &flagsReturned));
84 + }
85 +
86 + // Launch a relay for this accepted connection
87 + LaunchRelay(std::move(AcceptedSocket));
88 +
89 + // Go back to standby to accept the next connection
90 + State = relay::IOHandleStatus::Standby;
91 +}
92 +
93 +HANDLE PortRelayAcceptHandle::GetHandle() const
94 +{
95 + return Event.get();
96 +}
97 +
98 +void PortRelayAcceptHandle::LaunchRelay(wil::unique_socket&& AcceptedSocket)
99 +{
100 + WSL_LOG(
101 + "StartPortRelay",
102 + TraceLoggingValue(LinuxPort, "LinuxPort"),
103 + TraceLoggingValue(Family, "Family"),
104 + TraceLoggingValue(AcceptedSocket.get(), "Socket"));
105 +
106 + // Launch relay in a dedicated thread
107 + std::thread relayThread{
108 + [Socket = std::move(AcceptedSocket), VmId = VmId, LinuxPort = LinuxPort, RelayPort = RelayPort, Family = Family]() mutable {
109 + try
110 + {
111 + wslutil::SetThreadDescription(L"Port relay");
112 +
113 + // Connect to the HvSocket
114 + auto hvSocket = hvsocket::Connect(VmId, RelayPort);
115 +
116 + // Send relay start message
117 + LX_INIT_START_SOCKET_RELAY message{};
118 + message.Header.MessageType = LxInitMessageStartSocketRelay;
119 + message.Header.MessageSize = sizeof(message);
120 + message.Family = (Family == AF_INET) ? LX_AF_INET : LX_AF_INET6;
121 + message.Port = LinuxPort;
122 + message.BufferSize = 0x20000; // LOCALHOST_RELAY_BUFFER_SIZE
123 +
124 + socket::Send(hvSocket.get(), gslhelpers::struct_as_bytes(message));
125 +
126 + // Relay data between the two sockets
127 + relay::SocketRelay(Socket.get(), hvSocket.get(), message.BufferSize);
128 +
129 + WSL_LOG("StopPortRelay", TraceLoggingValue(LinuxPort, "LinuxPort"), TraceLoggingValue(Socket.get(), "Socket"));
130 + }
131 + CATCH_LOG();
132 + }};
133 +
134 + relayThread.detach();
135 +}
src/windows/wslcsession/PortRelayHandle.h new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + PortRelayHandle.h
8 +
9 +Abstract:
10 +
11 + Contains the definition of the PortRelayAcceptHandle class for port relaying.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "relay.hpp"
18 +
19 +namespace wsl::windows::service::wslc {
20 +
21 +class IORelay;
22 +
23 +class PortRelayAcceptHandle : public common::relay::OverlappedIOHandle
24 +{
25 +public:
26 + NON_COPYABLE(PortRelayAcceptHandle)
27 + NON_MOVABLE(PortRelayAcceptHandle)
28 +
29 + PortRelayAcceptHandle(wil::unique_socket&& ListenSocket, const GUID& VmId, uint32_t RelayPort, uint32_t LinuxPort, int Family, IORelay& IoRelay);
30 +
31 + ~PortRelayAcceptHandle();
32 +
33 + void Schedule() override;
34 + void Collect() override;
35 + HANDLE GetHandle() const override;
36 +
37 +private:
38 + void LaunchRelay(wil::unique_socket&& AcceptedSocket);
39 +
40 + wil::unique_socket ListenSocket;
41 + wil::unique_socket AcceptedSocket;
42 + GUID VmId;
43 + uint32_t RelayPort;
44 + uint32_t LinuxPort;
45 + int Family;
46 + IORelay& IoRelay;
47 + wil::unique_event Event{wil::EventOptions::ManualReset};
48 + OVERLAPPED Overlapped{};
49 + char AcceptBuffer[2 * sizeof(SOCKADDR_STORAGE)]{};
50 +};
51 +
52 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/ServiceProcessLauncher.cpp new
+76
@@ -0,0 +1,76 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ServiceProcessLauncher.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the implementations of ServiceProcessLauncher and ServiceRunningProcess.
12 +
13 +--*/
14 +
15 +#include "ServiceProcessLauncher.h"
16 +#include "WSLCVirtualMachine.h"
17 +
18 +using wsl::windows::service::wslc::ServiceProcessLauncher;
19 +using wsl::windows::service::wslc::ServiceRunningProcess;
20 +using wsl::windows::service::wslc::WSLCProcess;
21 +
22 +ServiceRunningProcess::ServiceRunningProcess(const Microsoft::WRL::ComPtr<WSLCProcess>& process, WSLCProcessFlags flags) :
23 + common::RunningWSLCProcess(flags)
24 +{
25 + process.CopyTo(m_process.GetAddressOf());
26 +}
27 +
28 +wil::unique_handle ServiceRunningProcess::GetStdHandle(int Index)
29 +{
30 + return std::move(Get().GetStdHandle(Index));
31 +}
32 +
33 +wil::unique_event ServiceRunningProcess::GetExitEvent()
34 +{
35 + // Unlike for std handles, the event handle needs to be duplicated, since we need to keep a reference to it
36 + // to signal it once the process exits.
37 + wil::unique_event event;
38 + THROW_IF_WIN32_BOOL_FALSE(
39 + DuplicateHandle(GetCurrentProcess(), m_process->GetExitEvent(), GetCurrentProcess(), &event, SYNCHRONIZE, false, 0));
40 +
41 + return event;
42 +}
43 +
44 +WSLCProcess& ServiceRunningProcess::Get()
45 +{
46 + return *m_process.Get();
47 +}
48 +
49 +void ServiceRunningProcess::GetState(WSLCProcessState* State, int* Code)
50 +{
51 + THROW_IF_FAILED(m_process->GetState(State, Code));
52 +}
53 +
54 +std::tuple<HRESULT, int, std::optional<ServiceRunningProcess>> ServiceProcessLauncher::LaunchNoThrow(WSLCVirtualMachine& virtualMachine)
55 +{
56 + auto [options, commandLine, env] = CreateProcessOptions();
57 + int error = -1;
58 +
59 + std::optional<ServiceRunningProcess> process;
60 + auto result = wil::ResultFromException(
61 + [&]() { process.emplace(virtualMachine.CreateLinuxProcess(m_executable.c_str(), options, &error), m_flags); });
62 +
63 + return {result, error, std::move(process)};
64 +}
65 +
66 +ServiceRunningProcess ServiceProcessLauncher::Launch(WSLCVirtualMachine& virtualMachine)
67 +{
68 + auto [hresult, error, process] = LaunchNoThrow(virtualMachine);
69 + if (FAILED(hresult))
70 + {
71 + auto commandLine = wsl::shared::string::Join(m_arguments, ' ');
72 + THROW_HR_MSG(hresult, "Failed to launch process: %hs (commandline: %hs). Errno = %i", m_executable.c_str(), commandLine.c_str(), error);
73 + }
74 +
75 + return std::move(process.value());
76 +}
\ No newline at end of file
src/windows/wslcsession/ServiceProcessLauncher.h new
+51
@@ -0,0 +1,51 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ServiceProcessLauncher.h
8 +
9 +Abstract:
10 +
11 + This file contains the definitions for ServiceProcessLauncher and ServiceRunningProcess.
12 +
13 +--*/
14 +
15 +#pragma once
16 +#include "WSLCProcessLauncher.h"
17 +#include "WSLCProcess.h"
18 +
19 +namespace wsl::windows::service::wslc {
20 +
21 +class WSLCVirtualMachine;
22 +
23 +class ServiceRunningProcess : public common::RunningWSLCProcess
24 +{
25 +public:
26 + NON_COPYABLE(ServiceRunningProcess);
27 + DEFAULT_MOVABLE(ServiceRunningProcess);
28 +
29 + ServiceRunningProcess(const Microsoft::WRL::ComPtr<WSLCProcess>& process, WSLCProcessFlags Flags);
30 + wil::unique_handle GetStdHandle(int Index) override;
31 + wil::unique_event GetExitEvent() override;
32 + WSLCProcess& Get();
33 +
34 +protected:
35 + void GetState(WSLCProcessState* State, int* Code) override;
36 +
37 +private:
38 + Microsoft::WRL::ComPtr<WSLCProcess> m_process;
39 +};
40 +
41 +class ServiceProcessLauncher : public common::WSLCProcessLauncher
42 +{
43 +public:
44 + NON_COPYABLE(ServiceProcessLauncher);
45 + NON_MOVABLE(ServiceProcessLauncher);
46 + using WSLCProcessLauncher::WSLCProcessLauncher;
47 +
48 + std::tuple<HRESULT, int, std::optional<ServiceRunningProcess>> LaunchNoThrow(WSLCVirtualMachine& virtualMachine);
49 + ServiceRunningProcess Launch(WSLCVirtualMachine& virtualMachine);
50 +};
51 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/WSLCContainer.cpp new
+2032
@@ -0,0 +1,2032 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCContainer.cpp
8 +
9 +Abstract:
10 +
11 + Contains the implementation of WSLCContainer.
12 + N.B. This class is designed to allow multiple container operations to run in parallel.
13 + Operations that don't change the state of the container must be const qualified, and acquire a shared lock on m_lock.
14 + Operations that do change the container's state must acquire m_lock exclusively.
15 + Operations that interact with processes inside the container or the init process must acquire m_processesLock.
16 + m_lock must always be acquired before m_processesLock
17 +
18 +--*/
19 +
20 +#include "precomp.h"
21 +#include "WSLCContainer.h"
22 +#include "WSLCProcess.h"
23 +#include "WSLCProcessIO.h"
24 +
25 +using wsl::windows::common::COMServiceExecutionContext;
26 +using wsl::windows::common::docker_schema::ErrorResponse;
27 +using wsl::windows::common::relay::DockerIORelayHandle;
28 +using wsl::windows::common::relay::HandleWrapper;
29 +using wsl::windows::common::relay::HTTPChunkBasedReadHandle;
30 +using wsl::windows::common::relay::OverlappedIOHandle;
31 +using wsl::windows::common::relay::ReadHandle;
32 +using wsl::windows::common::relay::RelayHandle;
33 +using wsl::windows::service::wslc::ContainerPortMapping;
34 +using wsl::windows::service::wslc::IWSLCVolume;
35 +using wsl::windows::service::wslc::RelayedProcessIO;
36 +using wsl::windows::service::wslc::TypedHandle;
37 +using wsl::windows::service::wslc::unique_com_disconnect;
38 +using wsl::windows::service::wslc::VMPortMapping;
39 +using wsl::windows::service::wslc::WSLCContainer;
40 +using wsl::windows::service::wslc::WSLCContainerImpl;
41 +using wsl::windows::service::wslc::WSLCContainerMetadata;
42 +using wsl::windows::service::wslc::WSLCContainerMetadataV1;
43 +using wsl::windows::service::wslc::WSLCPortMapping;
44 +using wsl::windows::service::wslc::WSLCSession;
45 +using wsl::windows::service::wslc::WSLCVirtualMachine;
46 +using wsl::windows::service::wslc::WSLCVolumeMount;
47 +
48 +using namespace wsl::windows::common::relay;
49 +using namespace wsl::windows::common::docker_schema;
50 +using namespace wsl::windows::common::wslutil;
51 +using namespace std::chrono_literals;
52 +using wsl::shared::Localization;
53 +
54 +namespace wslc_schema = wsl::windows::common::wslc_schema;
55 +
56 +using DockerInspectContainer = wsl::windows::common::docker_schema::InspectContainer;
57 +using WslcInspectContainer = wsl::windows::common::wslc_schema::InspectContainer;
58 +
59 +namespace {
60 +
61 +std::vector<std::string> StringArrayToVector(const WSLCStringArray& array)
62 +{
63 + if (array.Count == 0)
64 + {
65 + return {};
66 + }
67 +
68 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, array.Values, "StringArray.Values is null with Count=%lu", array.Count);
69 +
70 + std::vector<std::string> result;
71 + result.reserve(array.Count);
72 + for (ULONG i = 0; i < array.Count; i += 1)
73 + {
74 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, array.Values[i], "StringArray.Values[%lu] is null", i);
75 + result.emplace_back(array.Values[i]);
76 + }
77 +
78 + return result;
79 +}
80 +
81 +// Parses a Docker ExposedPorts key (e.g. "8080/tcp", "5432/udp") into port number and protocol.
82 +std::pair<uint16_t, int> ParseExposedPortKey(const std::string& key)
83 +{
84 + auto slashPos = key.find('/');
85 + THROW_HR_IF_MSG(E_INVALIDARG, slashPos == std::string::npos, "Invalid exposed port format: %hs", key.c_str());
86 +
87 + auto portStr = std::string_view(key.c_str(), slashPos);
88 +
89 + uint16_t port{};
90 + auto result = std::from_chars(portStr.data(), portStr.data() + portStr.size(), port);
91 + if (result.ec != std::errc{} || result.ptr != portStr.data() + portStr.size() || port == 0)
92 + {
93 + THROW_HR_MSG(E_INVALIDARG, "Invalid port number in exposed port: %hs", key.c_str());
94 + }
95 +
96 + auto protoStr = key.substr(slashPos + 1);
97 + int protocol{};
98 + if (protoStr == "tcp")
99 + {
100 + protocol = IPPROTO_TCP;
101 + }
102 + else if (protoStr == "udp")
103 + {
104 + protocol = IPPROTO_UDP;
105 + }
106 + else
107 + {
108 + THROW_HR_MSG(E_INVALIDARG, "Unsupported protocol in exposed port: %hs", key.c_str());
109 + }
110 +
111 + return {static_cast<uint16_t>(port), protocol};
112 +}
113 +
114 +// Temporary solution to allocate an ephemeral port.
115 +// TODO: Remove once the port relay can allocate ephemeral ports.
116 +uint16_t AllocateEphemeralPort(int family, const char* address)
117 +{
118 + wil::unique_socket sock(socket(family, SOCK_STREAM, IPPROTO_TCP));
119 + THROW_LAST_ERROR_IF(!sock);
120 +
121 + SOCKADDR_INET addr{};
122 + addr.si_family = static_cast<ADDRESS_FAMILY>(family);
123 +
124 + if (family == AF_INET)
125 + {
126 + THROW_HR_IF_MSG(E_INVALIDARG, inet_pton(AF_INET, address, &addr.Ipv4.sin_addr) != 1, "Failed to parse ip address: %hs", address);
127 + }
128 + else if (family == AF_INET6)
129 + {
130 + THROW_HR_IF_MSG(E_INVALIDARG, inet_pton(AF_INET6, address, &addr.Ipv6.sin6_addr) != 1, "Failed to parse ip address: %hs", address);
131 + }
132 + else
133 + {
134 + THROW_HR_MSG(E_UNEXPECTED, "Unexpected address family: %i", family);
135 + }
136 +
137 + THROW_LAST_ERROR_IF(bind(sock.get(), reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == SOCKET_ERROR);
138 +
139 + int addrLen = sizeof(addr);
140 + THROW_LAST_ERROR_IF(getsockname(sock.get(), reinterpret_cast<sockaddr*>(&addr), &addrLen) == SOCKET_ERROR);
141 +
142 + uint16_t port = (family == AF_INET6) ? ntohs(addr.Ipv6.sin6_port) : ntohs(addr.Ipv4.sin_port);
143 + THROW_HR_IF_MSG(E_UNEXPECTED, port == 0, "OS returned ephemeral port 0");
144 +
145 + return port;
146 +}
147 +
148 +// Builds port mapping list from container options and returns the network mode string.
149 +std::pair<std::vector<ContainerPortMapping>, std::string> ProcessPortMappings(
150 + std::vector<_WSLCPortMapping>& requestedPorts, WSLCContainerNetworkType networkType, WSLCVirtualMachine& virtualMachine)
151 +{
152 + // Determine network mode string.
153 + std::string networkMode;
154 + if (networkType == WSLCContainerNetworkTypeBridged)
155 + {
156 + networkMode = "bridge";
157 + }
158 + else if (networkType == WSLCContainerNetworkTypeHost)
159 + {
160 + networkMode = "host";
161 + }
162 + else if (networkType == WSLCContainerNetworkTypeNone)
163 + {
164 + networkMode = "none";
165 + }
166 + else
167 + {
168 + THROW_HR_MSG(E_INVALIDARG, "Invalid networking mode: %i", networkType);
169 + }
170 +
171 + // Validate port mappings.
172 + THROW_HR_IF_MSG(
173 + E_INVALIDARG,
174 + !requestedPorts.empty() && networkType == WSLCContainerNetworkTypeNone,
175 + "Port mappings are not supported without networking");
176 +
177 + std::vector<ContainerPortMapping> ports;
178 + ports.reserve(requestedPorts.size());
179 +
180 + for (auto& e : requestedPorts)
181 + {
182 + if (e.HostPort == WSLC_EPHEMERAL_PORT)
183 + {
184 + e.HostPort = AllocateEphemeralPort(e.Family, e.BindingAddress);
185 + }
186 +
187 + auto& entry = ports.emplace_back(VMPortMapping::FromWSLCPortMapping(e), e.ContainerPort);
188 +
189 + // Only allocate port for bridged network. Host mode ports are allocated when the container starts.
190 + if (networkType == WSLCContainerNetworkTypeBridged)
191 + {
192 + entry.VmMapping.AssignVmPort(virtualMachine.AllocatePort(e.Family, e.Protocol));
193 + }
194 + }
195 +
196 + return {std::move(ports), std::move(networkMode)};
197 +}
198 +
199 +void UnmountVolumes(std::vector<WSLCVolumeMount>& volumes, WSLCVirtualMachine& parentVM)
200 +{
201 + for (auto& volume : volumes)
202 + {
203 + if (volume.Mounted)
204 + {
205 + if (SUCCEEDED(LOG_IF_FAILED(parentVM.UnmountWindowsFolder(volume.ParentVMPath.c_str()))))
206 + {
207 + volume.Mounted = false;
208 + }
209 + }
210 + }
211 +}
212 +
213 +auto MountVolumes(std::vector<WSLCVolumeMount>& volumes, WSLCVirtualMachine& parentVM)
214 +{
215 + auto errorCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&volumes, &parentVM]() { UnmountVolumes(volumes, parentVM); });
216 +
217 + for (auto& volume : volumes)
218 + {
219 + // Create a new directory if it doesn't exist.
220 + if (!std::filesystem::exists(volume.HostPath))
221 + {
222 + auto result = wil::CreateDirectoryDeepNoThrow(volume.HostPath.c_str());
223 + if (FAILED(result))
224 + {
225 + THROW_HR_WITH_USER_ERROR(
226 + result, Localization::MessageWslcFailedToMountVolume(volume.HostPath, wsl::windows::common::wslutil::GetErrorString(result)));
227 + }
228 + }
229 +
230 + auto result = parentVM.MountWindowsFolder(volume.HostPath.c_str(), volume.ParentVMPath.c_str(), volume.ReadOnly);
231 + THROW_IF_FAILED_MSG(result, "Failed to mount %ls -> %hs", volume.HostPath.c_str(), volume.ParentVMPath.c_str());
232 + volume.Mounted = true;
233 + }
234 +
235 + return std::move(errorCleanup);
236 +}
237 +
238 +WSLCContainerState DockerStateToWSLCState(ContainerState state)
239 +{
240 + // TODO: Handle other states like Paused, Restarting, etc.
241 + switch (state)
242 + {
243 + case ContainerState::Created:
244 + return WSLCContainerState::WslcContainerStateCreated;
245 + case ContainerState::Running:
246 + return WSLCContainerState::WslcContainerStateRunning;
247 + case ContainerState::Exited:
248 + case ContainerState::Dead:
249 + return WSLCContainerState::WslcContainerStateExited;
250 + case ContainerState::Removing:
251 + return WSLCContainerState::WslcContainerStateDeleted;
252 + default:
253 + return WSLCContainerState::WslcContainerStateInvalid;
254 + }
255 +}
256 +
257 +WSLCContainerNetworkType DockerNetworkModeToWSLCNetworkType(const std::string& mode)
258 +{
259 + if (mode == "bridge")
260 + {
261 + return WSLCContainerNetworkTypeBridged;
262 + }
263 + else if (mode == "host")
264 + {
265 + return WSLCContainerNetworkTypeHost;
266 + }
267 + else if (mode == "none")
268 + {
269 + return WSLCContainerNetworkTypeNone;
270 + }
271 +
272 + THROW_HR_MSG(E_INVALIDARG, "Invalid networking mode: %hs", mode.c_str());
273 +}
274 +
275 +std::uint64_t ParseDockerTimestamp(const std::string& timestamp)
276 +{
277 + // Docker timestamps are UTC ISO 8601, e.g. "2026-03-05T10:30:00.123456789Z".
278 + std::chrono::sys_seconds utcSeconds;
279 + std::istringstream stream(timestamp);
280 + stream >> std::chrono::parse("%FT%H:%M:%S%Z", utcSeconds);
281 + THROW_HR_IF_MSG(E_INVALIDARG, stream.fail(), "Failed to parse timestamp '%hs'", timestamp.c_str());
282 +
283 + return static_cast<std::uint64_t>(utcSeconds.time_since_epoch().count());
284 +}
285 +
286 +std::string CleanContainerName(const std::string& name)
287 +{
288 + // Docker container names have a leading '/', strip it.
289 + if (!name.empty() && name[0] == '/')
290 + {
291 + return name.substr(1);
292 + }
293 +
294 + return name;
295 +}
296 +
297 +std::string ExtractContainerName(const std::vector<std::string>& names, const std::string& id)
298 +{
299 + if (names.empty())
300 + {
301 + return id;
302 + }
303 +
304 + return CleanContainerName(names[0]);
305 +}
306 +
307 +std::string FormatPortEndpoint(const ContainerPortMapping& portMapping)
308 +{
309 + auto addr = portMapping.VmMapping.BindingAddressString();
310 + return std::format(
311 + "{}:{}/{}",
312 + portMapping.VmMapping.IsIPv6() ? std::format("[{}]", addr) : addr,
313 + portMapping.VmMapping.HostPort(),
314 + portMapping.ProtocolString());
315 +}
316 +
317 +WSLCContainerMetadataV1 ParseContainerMetadata(const std::string& json)
318 +{
319 + auto wrapper = wsl::shared::FromJson<WSLCContainerMetadata>(json.c_str());
320 + THROW_HR_IF(E_UNEXPECTED, !wrapper.V1.has_value());
321 +
322 + return wrapper.V1.value();
323 +}
324 +
325 +std::string SerializeContainerMetadata(const WSLCContainerMetadataV1& metadata)
326 +{
327 + WSLCContainerMetadata wrapper;
328 + wrapper.V1 = metadata;
329 +
330 + return wsl::shared::ToJson(wrapper);
331 +}
332 +
333 +void ProcessNamedVolumes(
334 + const WSLCContainerOptions& containerOptions,
335 + const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
336 + wsl::windows::common::docker_schema::CreateContainer& request)
337 +{
338 + THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr);
339 +
340 + for (ULONG i = 0; i < containerOptions.NamedVolumesCount; i++)
341 + {
342 + const auto& nv = containerOptions.NamedVolumes[i];
343 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, nv.Name, "NamedVolume at index %lu has null Name", i);
344 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, nv.ContainerPath, "NamedVolume at index %lu has null ContainerPath", i);
345 +
346 + std::string volumeName = nv.Name;
347 +
348 + THROW_HR_WITH_USER_ERROR_IF(
349 + WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(nv.Name), !sessionVolumes.contains(volumeName));
350 +
351 + wsl::windows::common::docker_schema::Mount mount{};
352 + mount.Source = std::move(volumeName);
353 + mount.Target = std::string(nv.ContainerPath);
354 + mount.Type = "volume";
355 + mount.ReadOnly = static_cast<bool>(nv.ReadOnly);
356 +
357 + request.HostConfig.Mounts.emplace_back(mount);
358 + }
359 +}
360 +
361 +void ValidateNamedVolumes(
362 + const std::vector<wsl::windows::common::docker_schema::Mount>& mounts,
363 + const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
364 + const std::unordered_set<std::string>& anonymousVolumes)
365 +{
366 + for (const auto& mount : mounts)
367 + {
368 + if (mount.Type == "volume" && !mount.Name.empty())
369 + {
370 + THROW_HR_WITH_USER_ERROR_IF(
371 + WSLC_E_VOLUME_NOT_FOUND,
372 + Localization::MessageWslcVolumeNotFound(mount.Name),
373 + !sessionVolumes.contains(mount.Name) && !anonymousVolumes.contains(mount.Name));
374 + }
375 + }
376 +}
377 +
378 +} // namespace
379 +
380 +ContainerPortMapping::ContainerPortMapping(VMPortMapping&& VmMapping, uint16_t ContainerPort) :
381 + VmMapping(std::move(VmMapping)), ContainerPort(ContainerPort)
382 +{
383 +}
384 +
385 +ContainerPortMapping::ContainerPortMapping(ContainerPortMapping&& Other) :
386 + VmMapping(std::move(Other.VmMapping)), ContainerPort(Other.ContainerPort)
387 +{
388 +}
389 +
390 +ContainerPortMapping& ContainerPortMapping::operator=(ContainerPortMapping&& Other)
391 +{
392 + if (this != &Other)
393 + {
394 + VmMapping = std::move(Other.VmMapping);
395 + ContainerPort = Other.ContainerPort;
396 + }
397 + return *this;
398 +}
399 +
400 +const char* ContainerPortMapping::ProtocolString() const
401 +{
402 + if (VmMapping.Protocol == IPPROTO_TCP)
403 + {
404 + return "tcp";
405 + }
406 + else
407 + {
408 + WI_ASSERT(VmMapping.Protocol == IPPROTO_UDP);
409 + return "udp";
410 + }
411 +}
412 +
413 +unique_com_disconnect::unique_com_disconnect(Microsoft::WRL::ComPtr<WSLCContainer>&& wrapper) noexcept :
414 + m_wrapper(std::move(wrapper))
415 +{
416 +}
417 +
418 +unique_com_disconnect::~unique_com_disconnect() noexcept
419 +{
420 + if (m_wrapper)
421 + {
422 + m_wrapper->Disconnect();
423 + }
424 +}
425 +
426 +WSLCPortMapping ContainerPortMapping::Serialize() const
427 +{
428 + return WSLCPortMapping{
429 + .HostPort = VmMapping.HostPort(),
430 + .VmPort = VmMapping.VmPort ? VmMapping.VmPort->Port() : ContainerPort,
431 + .ContainerPort = ContainerPort,
432 + .Family = VmMapping.BindAddress.si_family,
433 + .Protocol = VmMapping.Protocol,
434 + .BindingAddress = VmMapping.BindingAddressString()};
435 +}
436 +
437 +WSLCContainerImpl::WSLCContainerImpl(
438 + WSLCSession& wslcSession,
439 + WSLCVirtualMachine& virtualMachine,
440 + std::string&& Id,
441 + std::string&& Name,
442 + std::string&& Image,
443 + WSLCContainerNetworkType NetworkMode,
444 + std::vector<WSLCVolumeMount>&& volumes,
445 + std::vector<ContainerPortMapping>&& ports,
446 + std::map<std::string, std::string>&& labels,
447 + std::function<void(const WSLCContainerImpl*)>&& onDeleted,
448 + ContainerEventTracker& EventTracker,
449 + DockerHTTPClient& DockerClient,
450 + IORelay& Relay,
451 + WSLCContainerState InitialState,
452 + std::uint64_t CreatedAt,
453 + WSLCProcessFlags InitProcessFlags,
454 + WSLCContainerFlags ContainerFlags) :
455 + m_wslcSession(wslcSession),
456 + m_virtualMachine(virtualMachine),
457 + m_name(std::move(Name)),
458 + m_image(std::move(Image)),
459 + m_networkingMode(NetworkMode),
460 + m_id(std::move(Id)),
461 + m_mountedVolumes(std::move(volumes)),
462 + m_mappedPorts(std::move(ports)),
463 + m_labels(std::move(labels)),
464 + m_comWrapper(wil::MakeOrThrow<WSLCContainer>(this, std::move(onDeleted))),
465 + m_dockerClient(DockerClient),
466 + m_eventTracker(EventTracker),
467 + m_ioRelay(Relay),
468 + m_containerEvents(EventTracker.RegisterContainerStateUpdates(
469 + m_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))),
470 + m_state(InitialState),
471 + m_createdAt(CreatedAt),
472 + m_initProcessFlags(InitProcessFlags),
473 + m_containerFlags(ContainerFlags)
474 +{
475 +}
476 +
477 +WSLCContainerImpl::~WSLCContainerImpl()
478 +{
479 + WSL_LOG(
480 + "~WSLCContainerImpl",
481 + TraceLoggingValue(m_name.c_str(), "Name"),
482 + TraceLoggingValue(m_id.c_str(), "Id"),
483 + TraceLoggingValue((int)m_state, "State"));
484 +
485 + // Snapshot and clear process references under the lock.
486 + // Callbacks are then invoked without holding m_lock.
487 + decltype(m_processes) processes;
488 + decltype(m_initProcessControl) initProcessControl = nullptr;
489 +
490 + {
491 + auto lock = m_lock.lock_exclusive();
492 + std::lock_guard processesLock{m_processesLock};
493 + initProcessControl = std::exchange(m_initProcessControl, nullptr);
494 + processes = std::exchange(m_processes, {});
495 + }
496 +
497 + if (initProcessControl)
498 + {
499 + initProcessControl->OnContainerReleased();
500 + }
501 +
502 + for (auto& process : processes)
503 + {
504 + process->OnContainerReleased();
505 + }
506 +
507 + m_containerEvents.Reset();
508 +
509 + // Release resources under m_lock, but extract the COM wrapper so Disconnect()
510 + // can be called without holding m_lock. Calling Disconnect() under m_lock can
511 + // deadlock if an in-flight COM caller is waiting for m_lock.
512 + unique_com_disconnect wrapper;
513 + {
514 + auto lock = m_lock.lock_exclusive();
515 + wrapper = ReleaseResources();
516 + }
517 +}
518 +
519 +void WSLCContainerImpl::OnProcessReleased(DockerExecProcessControl* process) noexcept
520 +{
521 + std::lock_guard processesLock{m_processesLock};
522 +
523 + auto remove = std::ranges::remove_if(m_processes, [process](const auto* e) { return e == process; });
524 + WI_ASSERT(remove.size() == 1);
525 +
526 + m_processes.erase(remove.begin(), remove.end());
527 +}
528 +
529 +const std::string& WSLCContainerImpl::Image() const noexcept
530 +{
531 + return m_image;
532 +}
533 +
534 +const std::string& WSLCContainerImpl::Name() const noexcept
535 +{
536 + return m_name;
537 +}
538 +
539 +std::vector<WSLCPortMapping> WSLCContainerImpl::GetPorts() const
540 +{
541 + auto lock = m_lock.lock_shared();
542 + if (m_state != WslcContainerStateRunning)
543 + {
544 + return {};
545 + }
546 +
547 + std::vector<WSLCPortMapping> result;
548 + result.reserve(m_mappedPorts.size());
549 + for (const auto& port : m_mappedPorts)
550 + {
551 + result.push_back(port.Serialize());
552 + }
553 + return result;
554 +}
555 +
556 +void WSLCContainerImpl::GetStateChangedAt(ULONGLONG* Result)
557 +{
558 + auto lock = m_lock.lock_shared();
559 + *Result = m_stateChangedAt;
560 +}
561 +
562 +void WSLCContainerImpl::GetCreatedAt(ULONGLONG* Result)
563 +{
564 + auto lock = m_lock.lock_shared();
565 + *Result = m_createdAt;
566 +}
567 +
568 +void WSLCContainerImpl::CopyTo(IWSLCContainer** Container) const
569 +{
570 + auto lock = m_lock.lock_shared();
571 +
572 + THROW_HR_IF_MSG(RPC_E_DISCONNECTED, m_comWrapper == nullptr, "Container '%hs' is being released", m_id.c_str());
573 +
574 + THROW_IF_FAILED(m_comWrapper.CopyTo(Container));
575 +}
576 +
577 +void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr) const
578 +{
579 + auto lock = m_lock.lock_shared();
580 +
581 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_RUNNING, Localization::MessageWslcContainerNotRunning(m_id.c_str()), m_state != WslcContainerStateRunning);
582 +
583 + wil::unique_socket ioHandle;
584 +
585 + try
586 + {
587 + ioHandle = m_dockerClient.AttachContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional<std::string>(DetachKeys));
588 + }
589 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to attach to container '%hs'", m_id.c_str());
590 +
591 + // If this is a TTY process, the PTY handle can be returned directly.
592 + if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty))
593 + {
594 + *Stdin = common::wslutil::ToCOMOutputHandle(
595 + reinterpret_cast<HANDLE>(ioHandle.get()), GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, WSLCHandleTypeSocket);
596 +
597 + return;
598 + }
599 +
600 + // Otherwise the stream is multiplexed and needs to be relayed.
601 + // TODO: Consider skipping stdin if the stdin flag isn't set.
602 + auto [stdinRead, stdinWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
603 + auto [stdoutRead, stdoutWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
604 + auto [stderrRead, stderrWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
605 +
606 + std::vector<std::unique_ptr<OverlappedIOHandle>> handles;
607 +
608 + // This is required for docker to know when stdin is closed.
609 + auto onInputComplete = [handle = ioHandle.get()]() { LOG_LAST_ERROR_IF(shutdown(handle, SD_SEND) == SOCKET_ERROR); };
610 +
611 + // N.B. Ownership of the io handle is given to the DockerIORelayHandle relay, so it can be closed when docker closes the connection.
612 + handles.emplace_back(
613 + std::make_unique<RelayHandle<ReadHandle>>(HandleWrapper{std::move(stdinRead), std::move(onInputComplete)}, ioHandle.get()));
614 +
615 + handles.emplace_back(std::make_unique<DockerIORelayHandle>(
616 + std::move(ioHandle), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::Raw));
617 +
618 + m_ioRelay.AddHandles(std::move(handles));
619 +
620 + *Stdin = common::wslutil::ToCOMOutputHandle(reinterpret_cast<HANDLE>(stdinWrite.get()), GENERIC_WRITE | SYNCHRONIZE, WSLCHandleTypePipe);
621 +
622 + *Stdout = common::wslutil::ToCOMOutputHandle(reinterpret_cast<HANDLE>(stdoutRead.get()), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
623 +
624 + *Stderr = common::wslutil::ToCOMOutputHandle(reinterpret_cast<HANDLE>(stderrRead.get()), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
625 +}
626 +
627 +void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys)
628 +{
629 + // Acquire an exclusive lock since this method modifies m_initProcessControl, m_initProcess and m_state.
630 + auto lock = m_lock.lock_exclusive();
631 +
632 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning);
633 +
634 + THROW_HR_IF_MSG(
635 + HRESULT_FROM_WIN32(ERROR_INVALID_STATE),
636 + m_state != WslcContainerStateCreated && m_state != WslcContainerStateExited,
637 + "Cannot start container '%hs', state %i",
638 + m_id.c_str(),
639 + m_state);
640 +
641 + // Attach to the container's init process so no IO is lost.
642 + std::unique_ptr<WSLCProcessIO> io;
643 +
644 + try
645 + {
646 + if (WI_IsFlagSet(Flags, WSLCContainerStartFlagsAttach))
647 + {
648 + auto detachKeys = DetachKeys == nullptr ? std::nullopt : std::optional<std::string>(DetachKeys);
649 +
650 + if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty))
651 + {
652 + io = std::make_unique<TTYProcessIO>(TypedHandle{
653 + wil::unique_handle{(HANDLE)m_dockerClient.AttachContainer(m_id, detachKeys).release()}, WSLCHandleTypeSocket});
654 + }
655 + else
656 + {
657 + wil::unique_handle stream{reinterpret_cast<HANDLE>(m_dockerClient.AttachContainer(m_id, detachKeys).release())};
658 + io = CreateRelayedProcessIO(std::move(stream), m_initProcessFlags);
659 + }
660 + }
661 + }
662 + catch (const DockerHTTPException& e)
663 + {
664 + // N.B. This can happen if 'DetachKeys' is invalid.
665 + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to attach to container '%hs' during start", m_id.c_str());
666 + }
667 +
668 + auto control = std::make_unique<DockerContainerProcessControl>(*this, m_dockerClient, m_eventTracker);
669 +
670 + std::lock_guard processesLock{m_processesLock};
671 + m_initProcessControl = control.get();
672 +
673 + m_initProcess = wil::MakeOrThrow<WSLCProcess>(std::move(control), std::move(io), m_initProcessFlags);
674 +
675 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() mutable {
676 + m_initProcess.Reset();
677 + m_initProcessControl = nullptr;
678 + });
679 +
680 + auto volumeCleanup = MountVolumes(m_mountedVolumes, m_virtualMachine);
681 +
682 + auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); });
683 + MapPorts();
684 +
685 + m_stopNotification.Event.ResetEvent();
686 + m_stopNotification.EventTime.store(0, std::memory_order_relaxed);
687 +
688 + try
689 + {
690 + m_dockerClient.StartContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional<std::string>(DetachKeys));
691 + }
692 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str());
693 +
694 + portCleanup.release();
695 + volumeCleanup.release();
696 +
697 + Transition(WslcContainerStateRunning);
698 + cleanup.release();
699 +}
700 +
701 +void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCode, std::uint64_t eventTime)
702 +{
703 + unique_com_disconnect comWrapper;
704 +
705 + if (event == ContainerEvent::Stop)
706 + {
707 + THROW_HR_IF(E_UNEXPECTED, !exitCode.has_value());
708 +
709 + std::unique_lock stopGuard{m_stopLock, std::try_to_lock};
710 +
711 + m_stopNotification.EventTime.store(eventTime, std::memory_order_release);
712 + m_stopNotification.Event.SetEvent();
713 +
714 + // If Stop() is already in flight, it will wake when the stop event is signaled and take care of cleanup.
715 + if (!stopGuard.owns_lock())
716 + {
717 + return;
718 + }
719 +
720 + auto lock = m_lock.lock_exclusive();
721 + auto previousState = m_state;
722 +
723 + // Don't run the deletion logic if the container is already in a stopped / deleted state.
724 + // This can happen if Delete() is called by the user.
725 + if (previousState == WslcContainerStateRunning)
726 + {
727 + Transition(WslcContainerStateExited, eventTime);
728 + ReleaseProcesses();
729 +
730 + ReleaseRuntimeResources();
731 +
732 + if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
733 + {
734 + comWrapper = DeleteExclusiveLockHeld(WSLCDeleteFlagsDeleteVolumes);
735 + }
736 + }
737 +
738 + // Release m_lock and m_stopLock before the wrapper's destructor calls
739 + // Disconnect(), so in-flight COM callers can drain from COMImplClass::m_callers.
740 + lock.reset();
741 + stopGuard.unlock();
742 + }
743 + else if (event == ContainerEvent::Destroy)
744 + {
745 + auto lock = m_lock.lock_exclusive();
746 + if (m_state != WslcContainerStateDeleted)
747 + {
748 + Transition(WslcContainerStateDeleted);
749 + }
750 + }
751 +
752 + WSL_LOG(
753 + "ContainerEvent",
754 + TraceLoggingValue(m_name.c_str(), "Name"),
755 + TraceLoggingValue(m_id.c_str(), "Id"),
756 + TraceLoggingValue((int)event, "Event"));
757 +}
758 +
759 +bool WSLCContainerImpl::WaitForEvent(const wil::unique_event& Event, std::chrono::milliseconds Timeout) const
760 +{
761 + const HANDLE waitHandles[] = {Event.get(), m_wslcSession.SessionTerminatingEvent()};
762 + const DWORD waitResult = WaitForMultipleObjects(RTL_NUMBER_OF(waitHandles), waitHandles, FALSE, gsl::narrow<DWORD>(Timeout.count()));
763 +
764 + switch (waitResult)
765 + {
766 + case WAIT_OBJECT_0:
767 + return true;
768 + case WAIT_OBJECT_0 + 1:
769 + THROW_HR_MSG(E_ABORT, "Session %lu is terminating.", m_wslcSession.Id());
770 + case WAIT_TIMEOUT:
771 + return false;
772 + default:
773 + THROW_LAST_ERROR();
774 + }
775 +}
776 +
777 +void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
778 +{
779 + // N.B. comWrapper must be destructed after m_lock is released.
780 + unique_com_disconnect comWrapper;
781 +
782 + std::lock_guard stopGuard{m_stopLock};
783 + auto lock = m_lock.lock_exclusive();
784 +
785 + if (m_state == WslcContainerStateExited && !Kill)
786 + {
787 + return;
788 + }
789 + else if (m_state != WslcContainerStateRunning)
790 + {
791 + THROW_HR_WITH_USER_ERROR_MSG(
792 + WSLC_E_CONTAINER_NOT_RUNNING,
793 + Localization::MessageWslcContainerNotRunning(m_id),
794 + "Cannot stop container '%hs', state: %i",
795 + m_id.c_str(),
796 + m_state);
797 + }
798 +
799 + std::optional<WSLCSignal> SignalArg;
800 + if (Signal != WSLCSignalNone)
801 + {
802 + SignalArg = Signal;
803 + }
804 +
805 + // Don't wait for the container to stop if we're not sending SIGKILL, since it may not stop the container.
806 + // N.B. If the signal was SIGTERM for instance, we'll receive the stop notification via OnEvent().
807 + bool waitForStop = !Kill || (SignalArg.value_or(WSLCSignalSIGKILL) == WSLCSignalSIGKILL);
808 +
809 + try
810 + {
811 + if (Kill)
812 + {
813 + m_dockerClient.SignalContainer(m_id, SignalArg);
814 +
815 + if (!waitForStop)
816 + {
817 + return;
818 + }
819 + }
820 + else
821 + {
822 + std::optional<ULONG> TimeoutArg;
823 + if (TimeoutSeconds >= 0)
824 + {
825 + TimeoutArg = static_cast<ULONG>(TimeoutSeconds);
826 + }
827 +
828 + m_dockerClient.StopContainer(m_id, SignalArg, TimeoutArg);
829 + }
830 + }
831 + catch (const DockerHTTPException& e)
832 + {
833 + // HTTP 304 is returned when the container is already stopped.
834 + if (Kill || e.StatusCode() != 304)
835 + {
836 + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to %hs container '%hs'", Kill ? "kill" : "stop", m_id.c_str());
837 + }
838 + }
839 +
840 + // Wait for the stop event to get the Docker timestamp.
841 + std::optional<std::uint64_t> stopTimestamp;
842 + if (WaitForEvent(m_stopNotification.Event, 60s))
843 + {
844 + stopTimestamp = m_stopNotification.EventTime.load(std::memory_order_acquire);
845 + }
846 +
847 + Transition(WslcContainerStateExited, stopTimestamp);
848 +
849 + ReleaseProcesses();
850 +
851 + ReleaseRuntimeResources();
852 +
853 + if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
854 + {
855 + comWrapper = DeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes);
856 + }
857 +}
858 +
859 +void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags)
860 +{
861 + auto lock = m_lock.lock_exclusive();
862 + auto wrapper = DeleteExclusiveLockHeld(Flags);
863 + lock.reset();
864 +
865 + // N.B. wrapper must be destroyed after m_lock is released, since its destructor calls Disconnect().
866 +}
867 +
868 +__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::DeleteExclusiveLockHeld(WSLCDeleteFlags Flags)
869 +{
870 + // Validate that the container is not running or already deleted.
871 + THROW_HR_WITH_USER_ERROR_IF(
872 + WSLC_E_CONTAINER_IS_RUNNING,
873 + Localization::MessageWslcCannotRemoveRunningContainer(m_id),
874 + m_state == WslcContainerStateRunning && WI_IsFlagClear(Flags, WSLCDeleteFlagsForce));
875 +
876 + THROW_HR_IF_MSG(
877 + HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_state == WslcContainerStateDeleted, "Container %hs is already deleted", m_id.c_str());
878 +
879 + WI_ASSERT(m_state != WslcContainerStateInvalid);
880 +
881 + try
882 + {
883 + m_dockerClient.DeleteContainer(m_id, WI_IsFlagSet(Flags, WSLCDeleteFlagsForce), WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes));
884 + }
885 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to delete container '%hs'", m_id.c_str());
886 +
887 + Transition(WslcContainerStateDeleted);
888 + return ReleaseResources();
889 +}
890 +
891 +void WSLCContainerImpl::Export(WSLCHandle OutHandle) const
892 +{
893 + auto lock = m_lock.lock_shared();
894 +
895 + // Validate that the container is not in the running state.
896 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning);
897 +
898 + std::pair<uint32_t, wil::unique_socket> SocketCodePair;
899 + SocketCodePair = m_dockerClient.ExportContainer(m_id);
900 +
901 + auto userHandle = m_wslcSession.OpenUserHandle(OutHandle);
902 +
903 + wsl::windows::common::relay::MultiHandleWait io = m_wslcSession.CreateIOContext();
904 +
905 + std::string errorJson;
906 + auto accumulateError = [&](const gsl::span<char>& buffer) {
907 + // If the export failed, accumulate the error message.
908 + errorJson.append(buffer.data(), buffer.size());
909 + };
910 +
911 + if (SocketCodePair.first != 200)
912 + {
913 + io.AddHandle(std::make_unique<ReadHandle>(HandleWrapper{std::move(SocketCodePair.second)}, std::move(accumulateError)));
914 + }
915 + else
916 + {
917 + io.AddHandle(
918 + std::make_unique<RelayHandle<HTTPChunkBasedReadHandle>>(HandleWrapper{std::move(SocketCodePair.second)}, userHandle.Get()),
919 + wsl::windows::common::relay::MultiHandleWait::CancelOnCompleted);
920 + }
921 +
922 + // Release the lock so the container can still be interacted with while the export is in progress.
923 + // Passed this point, no member variables can be accessed.
924 + lock.reset();
925 +
926 + io.Run({});
927 +
928 + if (SocketCodePair.first != 200)
929 + {
930 + // Export failed, parse the error message.
931 + auto error = wsl::shared::FromJson<common::docker_schema::ErrorResponse>(errorJson.c_str());
932 +
933 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, error.message, SocketCodePair.first == 404);
934 + THROW_HR_WITH_USER_ERROR(E_FAIL, error.message);
935 + }
936 +}
937 +
938 +void WSLCContainerImpl::GetState(WSLCContainerState* Result)
939 +{
940 + auto lock = m_lock.lock_shared();
941 + *Result = m_state;
942 +}
943 +
944 +WSLCContainerState WSLCContainerImpl::State() const noexcept
945 +{
946 + auto lock = m_lock.lock_shared();
947 + return m_state;
948 +}
949 +
950 +void WSLCContainerImpl::GetInitProcess(IWSLCProcess** Process) const
951 +{
952 + auto lock = m_lock.lock_shared();
953 + std::lock_guard processesLock{m_processesLock};
954 +
955 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_initProcess);
956 + THROW_IF_FAILED(m_initProcess.CopyTo(__uuidof(IWSLCProcess), (void**)Process));
957 +}
958 +
959 +void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKeys, IWSLCProcess** Process)
960 +{
961 + THROW_HR_IF_MSG(E_INVALIDARG, Options->CommandLine.Count == 0, "Exec command line cannot be empty");
962 +
963 + auto lock = m_lock.lock_shared();
964 +
965 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_RUNNING, Localization::MessageWslcContainerNotRunning(m_id), m_state != WslcContainerStateRunning);
966 +
967 + common::docker_schema::CreateExec request{};
968 + request.AttachStdout = true;
969 + request.AttachStderr = true;
970 +
971 + request.Cmd = StringArrayToVector(Options->CommandLine);
972 + request.Env = StringArrayToVector(Options->Environment);
973 +
974 + if (Options->CurrentDirectory != nullptr)
975 + {
976 + request.WorkingDir = Options->CurrentDirectory;
977 + }
978 +
979 + if (Options->User != nullptr)
980 + {
981 + request.User = Options->User;
982 + }
983 +
984 + if (WI_IsFlagSet(Options->Flags, WSLCProcessFlagsTty))
985 + {
986 + request.Tty = true;
987 + }
988 +
989 + if (WI_IsFlagSet(Options->Flags, WSLCProcessFlagsStdin))
990 + {
991 + request.AttachStdin = true;
992 + }
993 +
994 + if (DetachKeys != nullptr)
995 + {
996 + request.DetachKeys = DetachKeys;
997 + }
998 +
999 + try
1000 + {
1001 + auto result = m_dockerClient.CreateExec(m_id, request);
1002 +
1003 + // N.B. There's no way to delete a created exec instance, it is removed when the container is deleted.
1004 +
1005 + wil::unique_handle stream{
1006 + (HANDLE)m_dockerClient
1007 + .StartExec(result.Id, common::docker_schema::StartExec{.Tty = request.Tty, .ConsoleSize = request.ConsoleSize})
1008 + .release()};
1009 +
1010 + std::unique_ptr<WSLCProcessIO> io;
1011 + if (request.Tty)
1012 + {
1013 + io = std::make_unique<TTYProcessIO>(TypedHandle{std::move(stream), WSLCHandleTypeSocket});
1014 + }
1015 + else
1016 + {
1017 + io = CreateRelayedProcessIO(std::move(stream), Options->Flags);
1018 + }
1019 +
1020 + auto control = std::make_unique<DockerExecProcessControl>(*this, result.Id, m_dockerClient, m_eventTracker);
1021 +
1022 + {
1023 + std::lock_guard processesLock{m_processesLock};
1024 +
1025 + // Store a non owning reference to the process.
1026 + m_processes.push_back(control.get());
1027 + }
1028 +
1029 + // Poll for the exec'd process to either be running, or failed.
1030 + // This is required because StartExec() returns before the process is actually created, and if exec() fails, we'll never
1031 + // get an exec_die notification, so this case needs to be caught before returning the process to the caller.
1032 +
1033 + // TODO: Configurable timeout.
1034 + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
1035 +
1036 + do
1037 + {
1038 + auto state = m_dockerClient.InspectExec(result.Id);
1039 + if (state.Running && state.Pid.has_value())
1040 + {
1041 + control->SetPid(state.Pid.value());
1042 + break; // Exec is running, exit.
1043 + }
1044 + else if (state.ExitCode.has_value())
1045 + {
1046 + control->SetExitCode(state.ExitCode.value());
1047 + break; // Exec has exited, exit.
1048 + }
1049 + else if (std::chrono::steady_clock::now() > deadline)
1050 + {
1051 + THROW_HR_MSG(
1052 + HRESULT_FROM_WIN32(ERROR_TIMEOUT),
1053 + "Timed out waiting for exec state for '%hs'. Last state: %hs",
1054 + result.Id.c_str(),
1055 + wsl::shared::ToJson(state).c_str());
1056 + }
1057 +
1058 + } while (!control->GetExitEvent().wait(100));
1059 +
1060 + auto process = wil::MakeOrThrow<WSLCProcess>(std::move(control), std::move(io), Options->Flags);
1061 + THROW_IF_FAILED(process.CopyTo(__uuidof(IWSLCProcess), (void**)Process));
1062 + }
1063 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to exec process in container %hs", m_id.c_str());
1064 +}
1065 +
1066 +WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspectContainer& dockerInspect) const
1067 +{
1068 + WslcInspectContainer wslcInspect{};
1069 +
1070 + wslcInspect.Id = dockerInspect.Id;
1071 + wslcInspect.Name = CleanContainerName(dockerInspect.Name);
1072 + wslcInspect.Created = dockerInspect.Created;
1073 + wslcInspect.Image = m_image;
1074 +
1075 + // Map container state.
1076 + wslcInspect.State.Status = dockerInspect.State.Status;
1077 + wslcInspect.State.Running = dockerInspect.State.Running;
1078 + wslcInspect.State.ExitCode = dockerInspect.State.ExitCode;
1079 + wslcInspect.State.StartedAt = dockerInspect.State.StartedAt;
1080 + wslcInspect.State.FinishedAt = dockerInspect.State.FinishedAt;
1081 +
1082 + wslcInspect.HostConfig.NetworkMode = dockerInspect.HostConfig.NetworkMode;
1083 +
1084 + // Map WSLC port mappings (Windows host ports only). HostIp is not set here and will use
1085 + // the default value ("127.0.0.1") defined in the InspectPortBinding schema.
1086 + for (const auto& e : m_mappedPorts)
1087 + {
1088 + // TODO: ipv6 support.
1089 + auto portKey = std::format("{}/{}", e.ContainerPort, e.ProtocolString());
1090 +
1091 + wslc_schema::InspectPortBinding portBinding{};
1092 + portBinding.HostPort = std::to_string(e.VmMapping.HostPort());
1093 +
1094 + wslcInspect.Ports[portKey].push_back(std::move(portBinding));
1095 + }
1096 +
1097 + // Map volume mounts using WSLC's host-side data.
1098 + wslcInspect.Mounts.reserve(m_mountedVolumes.size() + dockerInspect.HostConfig.Tmpfs.size());
1099 + for (const auto& volume : m_mountedVolumes)
1100 + {
1101 + wslc_schema::InspectMount mountInfo{};
1102 + // TODO: Support different mount types (plan9/VHD) when VHD volumes are implemented.
1103 + mountInfo.Type = "bind";
1104 +
1105 + // For file mounts, reconstruct the original host path from the parent directory and filename.
1106 + if (volume.SourceFilename.empty())
1107 + {
1108 + mountInfo.Source = wsl::shared::string::WideToMultiByte(volume.HostPath);
1109 + }
1110 + else
1111 + {
1112 + std::filesystem::path fullPath(volume.HostPath);
1113 + fullPath /= volume.SourceFilename;
1114 + mountInfo.Source = fullPath.string();
1115 + }
1116 +
1117 + mountInfo.Destination = volume.ContainerPath;
1118 + mountInfo.ReadWrite = !volume.ReadOnly;
1119 + wslcInspect.Mounts.push_back(std::move(mountInfo));
1120 + }
1121 +
1122 + // Map tmpfs mounts from Docker inspect data.
1123 + for (const auto& entry : dockerInspect.HostConfig.Tmpfs)
1124 + {
1125 + wslc_schema::InspectMount mountInfo{};
1126 + mountInfo.Type = "tmpfs";
1127 + mountInfo.Destination = entry.first;
1128 + // Tmpfs mounts are read-write by default. We currently do not parse tmpfs options
1129 + // (e.g. "ro") for inspect output; Docker enforces actual mount behavior.
1130 + mountInfo.ReadWrite = true;
1131 + wslcInspect.Mounts.push_back(std::move(mountInfo));
1132 + }
1133 +
1134 + // Map labels. m_labels should already exclude internal metadata labels.
1135 + wslcInspect.Labels = m_labels;
1136 +
1137 + return wslcInspect;
1138 +}
1139 +
1140 +std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1141 + const WSLCContainerOptions& containerOptions,
1142 + WSLCSession& wslcSession,
1143 + WSLCVirtualMachine& virtualMachine,
1144 + const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
1145 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
1146 + ContainerEventTracker& EventTracker,
1147 + DockerHTTPClient& DockerClient,
1148 + IORelay& IoRelay)
1149 +{
1150 + common::docker_schema::CreateContainer request;
1151 + request.Image = containerOptions.Image;
1152 +
1153 + // TODO: Think about when 'StdinOnce' should be set.
1154 + request.StdinOnce = true;
1155 +
1156 + if (WI_IsFlagSet(containerOptions.InitProcessOptions.Flags, WSLCProcessFlagsTty))
1157 + {
1158 + request.Tty = true;
1159 + }
1160 +
1161 + if (WI_IsFlagSet(containerOptions.InitProcessOptions.Flags, WSLCProcessFlagsStdin))
1162 + {
1163 + request.OpenStdin = true;
1164 + }
1165 +
1166 + if (containerOptions.InitProcessOptions.CommandLine.Count > 0)
1167 + {
1168 + request.Cmd = StringArrayToVector(containerOptions.InitProcessOptions.CommandLine);
1169 + }
1170 +
1171 + if (containerOptions.Entrypoint.Count > 0)
1172 + {
1173 + request.Entrypoint = StringArrayToVector(containerOptions.Entrypoint);
1174 + }
1175 +
1176 + request.Env = StringArrayToVector(containerOptions.InitProcessOptions.Environment);
1177 +
1178 + if (containerOptions.StopSignal != WSLCSignalNone)
1179 + {
1180 + request.StopSignal = std::to_string(containerOptions.StopSignal);
1181 + }
1182 +
1183 + if (containerOptions.InitProcessOptions.CurrentDirectory != nullptr)
1184 + {
1185 + request.WorkingDir = containerOptions.InitProcessOptions.CurrentDirectory;
1186 + }
1187 +
1188 + if (containerOptions.HostName != nullptr)
1189 + {
1190 + request.Hostname = containerOptions.HostName;
1191 + }
1192 +
1193 + if (containerOptions.DomainName != nullptr)
1194 + {
1195 + request.Domainname = containerOptions.DomainName;
1196 + }
1197 +
1198 + if (containerOptions.DnsServers.Count > 0)
1199 + {
1200 + THROW_HR_IF_NULL_MSG(
1201 + E_INVALIDARG,
1202 + containerOptions.DnsServers.Values,
1203 + "DnsServers.Values is null with Count=%lu",
1204 + containerOptions.DnsServers.Count);
1205 +
1206 + request.HostConfig.Dns = StringArrayToVector(containerOptions.DnsServers);
1207 + }
1208 +
1209 + if (containerOptions.DnsSearchDomains.Count > 0)
1210 + {
1211 + THROW_HR_IF_NULL_MSG(
1212 + E_INVALIDARG,
1213 + containerOptions.DnsSearchDomains.Values,
1214 + "DnsSearchDomains.Values is null with Count=%lu",
1215 + containerOptions.DnsSearchDomains.Count);
1216 +
1217 + request.HostConfig.DnsSearch = StringArrayToVector(containerOptions.DnsSearchDomains);
1218 + }
1219 +
1220 + if (containerOptions.DnsOptions.Count > 0)
1221 + {
1222 + THROW_HR_IF_NULL_MSG(
1223 + E_INVALIDARG,
1224 + containerOptions.DnsOptions.Values,
1225 + "DnsOptions.Values is null with Count=%lu",
1226 + containerOptions.DnsOptions.Count);
1227 +
1228 + request.HostConfig.DnsOptions = StringArrayToVector(containerOptions.DnsOptions);
1229 + }
1230 +
1231 + if (containerOptions.InitProcessOptions.User != nullptr)
1232 + {
1233 + request.User = containerOptions.InitProcessOptions.User;
1234 + }
1235 +
1236 + request.HostConfig.Init = WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsInit);
1237 +
1238 + if (containerOptions.VolumesCount > 0)
1239 + {
1240 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes, "Volumes is null with VolumesCount=%lu", containerOptions.VolumesCount);
1241 + }
1242 +
1243 + // Build volume list from container options.
1244 + std::vector<WSLCVolumeMount> volumes;
1245 + volumes.reserve(containerOptions.VolumesCount);
1246 +
1247 + std::vector<std::string> binds;
1248 + binds.reserve(containerOptions.VolumesCount);
1249 +
1250 + for (ULONG i = 0; i < containerOptions.VolumesCount; i++)
1251 + {
1252 + GUID volumeId;
1253 + THROW_IF_FAILED(CoCreateGuid(&volumeId));
1254 +
1255 + auto parentVMPath = std::format("/mnt/{}", wsl::shared::string::GuidToString<char>(volumeId));
1256 + auto volume = containerOptions.Volumes[i];
1257 +
1258 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, volume.HostPath, "Volumes[%lu].HostPath is null", i);
1259 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, volume.ContainerPath, "Volumes[%lu].ContainerPath is null", i);
1260 +
1261 + std::filesystem::path hostPath = volume.HostPath;
1262 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(volume.HostPath), !hostPath.is_absolute());
1263 +
1264 + std::wstring sourceFilename;
1265 +
1266 + {
1267 + // Resolve symlinks.
1268 + std::error_code ec;
1269 + hostPath = std::filesystem::canonical(hostPath, ec);
1270 + if (!ec)
1271 + {
1272 + // When the host path is a file, mount the parent directory in the VM
1273 + // and bind only the specific file into the container via Docker.
1274 + if (std::filesystem::is_regular_file(hostPath))
1275 + {
1276 + sourceFilename = hostPath.filename().wstring();
1277 + hostPath = hostPath.parent_path();
1278 + }
1279 + }
1280 + else
1281 + {
1282 + if (ec == std::errc::no_such_file_or_directory)
1283 + {
1284 + // Path doesn't exist, assume directory.
1285 + hostPath = volume.HostPath;
1286 + }
1287 + else
1288 + {
1289 + THROW_HR_WITH_USER_ERROR(E_FAIL, Localization::MessageWslcFailedToMountVolume(volume.HostPath, ec.message()));
1290 + }
1291 + }
1292 + }
1293 +
1294 + volumes.push_back(WSLCVolumeMount{hostPath, parentVMPath, volume.ContainerPath, static_cast<bool>(volume.ReadOnly), sourceFilename});
1295 +
1296 + auto options = volume.ReadOnly ? "ro" : "rw";
1297 + auto bindSource = sourceFilename.empty() ? parentVMPath : std::format("{}/{}", parentVMPath, sourceFilename);
1298 + auto bind = std::format("{}:{}:{}", bindSource, volume.ContainerPath, options);
1299 +
1300 + binds.push_back(std::move(bind));
1301 + }
1302 +
1303 + request.HostConfig.Binds = std::move(binds);
1304 +
1305 + // Process tmpfs mounts from container options.
1306 + if (containerOptions.TmpfsCount > 0)
1307 + {
1308 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Tmpfs, "Tmpfs is null with TmpfsCount=%lu", containerOptions.TmpfsCount);
1309 +
1310 + for (ULONG i = 0; i < containerOptions.TmpfsCount; i++)
1311 + {
1312 + const auto& tmpfs = containerOptions.Tmpfs[i];
1313 +
1314 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, tmpfs.Destination, "Tmpfs mount at index %lu has null destination", i);
1315 +
1316 + request.HostConfig.Tmpfs[tmpfs.Destination] = tmpfs.Options != nullptr ? tmpfs.Options : "";
1317 + }
1318 + }
1319 +
1320 + ProcessNamedVolumes(containerOptions, sessionVolumes, request);
1321 +
1322 + // Prepare port mappings from container options.
1323 + std::vector<_WSLCPortMapping> ports;
1324 + for (ULONG i = 0; i < containerOptions.PortsCount; i++)
1325 + {
1326 + auto& port = ports.emplace_back();
1327 + port.HostPort = containerOptions.Ports[i].HostPort;
1328 + port.ContainerPort = containerOptions.Ports[i].ContainerPort;
1329 + port.Family = containerOptions.Ports[i].Family;
1330 + port.Protocol = containerOptions.Ports[i].Protocol;
1331 + strcpy_s(port.BindingAddress, containerOptions.Ports[i].BindingAddress);
1332 + }
1333 +
1334 + // Append exposed ports from the image, if requested.
1335 + if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsPublishAll))
1336 + {
1337 + auto imageInfo = DockerClient.InspectImage(containerOptions.Image);
1338 +
1339 + // Use the resolved image ID so the container is created from the exact same image.
1340 + request.Image = imageInfo.Id;
1341 +
1342 + if (imageInfo.Config.has_value() && imageInfo.Config->ExposedPorts.has_value())
1343 + {
1344 + for (const auto& [portKey, _] : imageInfo.Config->ExposedPorts.value())
1345 + {
1346 + auto [port, protocol] = ParseExposedPortKey(portKey);
1347 +
1348 + // Only TCP localhost mappings are currently supported by the relay path.
1349 + if (protocol != IPPROTO_TCP)
1350 + {
1351 + continue;
1352 + }
1353 +
1354 + auto& createdPort = ports.emplace_back();
1355 + createdPort.HostPort = WSLC_EPHEMERAL_PORT;
1356 + createdPort.Family = AF_INET;
1357 + createdPort.ContainerPort = port;
1358 + createdPort.Protocol = protocol;
1359 + strcpy_s(createdPort.BindingAddress, "127.0.0.1");
1360 + }
1361 + }
1362 + }
1363 +
1364 + // Process port mappings from container options.
1365 + auto [mappedPorts, networkMode] = ProcessPortMappings(ports, containerOptions.ContainerNetwork.ContainerNetworkType, virtualMachine);
1366 +
1367 + request.HostConfig.NetworkMode = networkMode;
1368 +
1369 + for (const auto& e : mappedPorts)
1370 + {
1371 + auto portKey = std::format("{}/{}", e.ContainerPort, e.ProtocolString());
1372 + request.ExposedPorts[portKey] = {};
1373 +
1374 + auto& portEntry = request.HostConfig.PortBindings[portKey];
1375 +
1376 + // In host mode, VmPort is empty until the container starts.
1377 + // In that networking mode, the host port always matches the vm port.
1378 + auto hostPort = e.VmMapping.VmPort ? e.VmMapping.VmPort->Port() : e.VmMapping.HostPort();
1379 +
1380 + portEntry.emplace_back(
1381 + common::docker_schema::PortMapping{.HostIp = e.VmMapping.BindingAddressString(), .HostPort = std::to_string(hostPort)});
1382 + }
1383 +
1384 + auto labels = ParseKeyValuePairs(containerOptions.Labels, containerOptions.LabelsCount, WSLCContainerMetadataLabel);
1385 +
1386 + // Build WSLC metadata to store in a label for recovery on Open().
1387 + WSLCContainerMetadataV1 metadata;
1388 + metadata.Flags = containerOptions.Flags;
1389 + metadata.InitProcessFlags = containerOptions.InitProcessOptions.Flags;
1390 + metadata.Volumes = volumes;
1391 +
1392 + for (const auto& e : mappedPorts)
1393 + {
1394 + metadata.Ports.emplace_back(e.Serialize());
1395 + }
1396 +
1397 + request.Labels[WSLCContainerMetadataLabel] = SerializeContainerMetadata(metadata);
1398 + request.Labels.insert(labels.begin(), labels.end());
1399 +
1400 + // Send the request to docker.
1401 + auto result =
1402 + DockerClient.CreateContainer(request, containerOptions.Name != nullptr ? containerOptions.Name : std::optional<std::string>{});
1403 +
1404 + // Clean up the Docker container if anything below fails.
1405 + // N.B. The container ID is captured by value since it is moved into the WSLCContainerImpl constructor below.
1406 + auto deleteOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&DockerClient, containerId = result.Id]() {
1407 + DockerClient.DeleteContainer(containerId, true, true);
1408 + });
1409 +
1410 + // Inspect the container to fetch its generated name (if needed) and Docker's authoritative Created timestamp.
1411 + auto inspectData = DockerClient.InspectContainer(result.Id);
1412 +
1413 + auto container = std::make_unique<WSLCContainerImpl>(
1414 + wslcSession,
1415 + virtualMachine,
1416 + std::move(result.Id),
1417 + CleanContainerName(inspectData.Name),
1418 + std::string(containerOptions.Image),
1419 + containerOptions.ContainerNetwork.ContainerNetworkType,
1420 + std::move(volumes),
1421 + std::move(mappedPorts),
1422 + std::move(labels),
1423 + std::move(OnDeleted),
1424 + EventTracker,
1425 + DockerClient,
1426 + IoRelay,
1427 + WslcContainerStateCreated,
1428 + ParseDockerTimestamp(inspectData.Created),
1429 + containerOptions.InitProcessOptions.Flags,
1430 + containerOptions.Flags);
1431 +
1432 + deleteOnFailure.release();
1433 + return container;
1434 +}
1435 +
1436 +std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
1437 + const common::docker_schema::ContainerInfo& dockerContainer,
1438 + WSLCSession& wslcSession,
1439 + WSLCVirtualMachine& virtualMachine,
1440 + const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
1441 + const std::unordered_set<std::string>& anonymousVolumes,
1442 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
1443 + ContainerEventTracker& EventTracker,
1444 + DockerHTTPClient& DockerClient,
1445 + IORelay& ioRelay)
1446 +{
1447 + // Extract container name from Docker's names list.
1448 + std::string name = ExtractContainerName(dockerContainer.Names, dockerContainer.Id);
1449 +
1450 + ValidateNamedVolumes(dockerContainer.Mounts, sessionVolumes, anonymousVolumes);
1451 +
1452 + auto labels(dockerContainer.Labels);
1453 + auto metadataIt = labels.find(WSLCContainerMetadataLabel);
1454 +
1455 + THROW_HR_IF_MSG(
1456 + E_INVALIDARG,
1457 + metadataIt == labels.end(),
1458 + "Cannot open WSLC container %hs: missing WSLC metadata label",
1459 + dockerContainer.Id.c_str());
1460 +
1461 + WI_ASSERT(dockerContainer.State != ContainerState::Running);
1462 +
1463 + auto metadata = ParseContainerMetadata(metadataIt->second.c_str());
1464 + labels.erase(metadataIt);
1465 +
1466 + auto networkingMode = DockerNetworkModeToWSLCNetworkType(dockerContainer.HostConfig.NetworkMode);
1467 + // Re-register recovered VM ports in the allocation pool to prevent conflicts.
1468 + std::vector<ContainerPortMapping> ports;
1469 + for (const auto& e : metadata.Ports)
1470 + {
1471 + auto& inserted = ports.emplace_back(ContainerPortMapping{VMPortMapping::FromContainerMetaData(e), e.ContainerPort});
1472 +
1473 + if (networkingMode == WSLCContainerNetworkTypeBridged)
1474 + {
1475 + auto allocation = virtualMachine.TryAllocatePort(e.VmPort, e.Family, e.Protocol);
1476 +
1477 + THROW_HR_IF_MSG(
1478 + HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS),
1479 + !allocation,
1480 + "Port %hu is in use, cannot open container %hs",
1481 + e.VmPort,
1482 + dockerContainer.Id.c_str());
1483 +
1484 + inserted.VmMapping.AssignVmPort(allocation);
1485 + }
1486 + }
1487 +
1488 + auto container = std::make_unique<WSLCContainerImpl>(
1489 + wslcSession,
1490 + virtualMachine,
1491 + std::string(dockerContainer.Id),
1492 + std::move(name),
1493 + std::string(dockerContainer.Image),
1494 + networkingMode,
1495 + std::move(metadata.Volumes),
1496 + std::move(ports),
1497 + std::move(labels),
1498 + std::move(OnDeleted),
1499 + EventTracker,
1500 + DockerClient,
1501 + ioRelay,
1502 + DockerStateToWSLCState(dockerContainer.State),
1503 + static_cast<std::uint64_t>(dockerContainer.Created),
1504 + metadata.InitProcessFlags,
1505 + metadata.Flags);
1506 +
1507 + // Restore the state change timestamp from Docker inspect data.
1508 + try
1509 + {
1510 + auto inspectData = DockerClient.InspectContainer(dockerContainer.Id);
1511 + auto state = DockerStateToWSLCState(dockerContainer.State);
1512 + const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt;
1513 +
1514 + if (!timestamp.empty())
1515 + {
1516 + container->m_stateChangedAt = ParseDockerTimestamp(timestamp);
1517 + }
1518 + }
1519 + CATCH_LOG();
1520 +
1521 + return container;
1522 +}
1523 +
1524 +const std::string& WSLCContainerImpl::ID() const noexcept
1525 +{
1526 + return m_id;
1527 +}
1528 +
1529 +void WSLCContainerImpl::Inspect(LPSTR* Output) const
1530 +{
1531 + auto lock = m_lock.lock_shared();
1532 +
1533 + try
1534 + {
1535 + // Get Docker inspect data
1536 + auto dockerInspect = m_dockerClient.InspectContainer(m_id);
1537 +
1538 + // Convert to WSLC schema
1539 + auto wslcInspect = BuildInspectContainer(dockerInspect);
1540 +
1541 + // Serialize WSLC schema to JSON
1542 + std::string wslcJson = wsl::shared::ToJson(wslcInspect);
1543 + *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(wslcJson.c_str()).release();
1544 + }
1545 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to inspect container '%hs'", m_id.c_str());
1546 +}
1547 +
1548 +void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const
1549 +{
1550 + auto lock = m_lock.lock_shared();
1551 +
1552 + wil::unique_socket socket;
1553 + try
1554 + {
1555 + socket = m_dockerClient.ContainerLogs(m_id, Flags, Since, Until, Tail);
1556 + }
1557 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to get logs from '%hs'", m_id.c_str());
1558 +
1559 + if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty))
1560 + {
1561 + // For tty processes, simply relay the HTTP chunks.
1562 + auto [ttyRead, ttyWrite] = common::wslutil::OpenAnonymousPipe(0, true, true);
1563 +
1564 + auto handle = std::make_unique<RelayHandle<HTTPChunkBasedReadHandle>>(std::move(socket), std::move(ttyWrite));
1565 + m_ioRelay.AddHandle(std::move(handle));
1566 +
1567 + *Stdout = common::wslutil::ToCOMOutputHandle(ttyRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
1568 + }
1569 + else
1570 + {
1571 + // For non-tty process, stdout & stderr are multiplexed.
1572 + auto [stdoutRead, stdoutWrite] = common::wslutil::OpenAnonymousPipe(0, true, true);
1573 + auto [stderrRead, stderrWrite] = common::wslutil::OpenAnonymousPipe(0, true, true);
1574 +
1575 + auto handle = std::make_unique<DockerIORelayHandle>(
1576 + std::move(socket), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::HttpChunked);
1577 +
1578 + m_ioRelay.AddHandle(std::move(handle));
1579 +
1580 + *Stdout = common::wslutil::ToCOMOutputHandle(stdoutRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
1581 + *Stderr = common::wslutil::ToCOMOutputHandle(stderrRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
1582 + }
1583 +}
1584 +
1585 +std::unique_ptr<RelayedProcessIO> WSLCContainerImpl::CreateRelayedProcessIO(wil::unique_handle&& stream, WSLCProcessFlags flags)
1586 +{
1587 + // Create one pipe for each STD handle.
1588 + std::vector<std::unique_ptr<OverlappedIOHandle>> ioHandles;
1589 + std::map<ULONG, TypedHandle> fds;
1590 +
1591 + // This is required for docker to know when stdin is closed.
1592 + auto closeStdin = [socket = stream.get(), this]() {
1593 + LOG_LAST_ERROR_IF(shutdown(reinterpret_cast<SOCKET>(socket), SD_SEND) == SOCKET_ERROR);
1594 + };
1595 +
1596 + if (WI_IsFlagSet(flags, WSLCProcessFlagsStdin))
1597 + {
1598 + auto [stdinRead, stdinWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
1599 + ioHandles.emplace_back(
1600 + std::make_unique<RelayHandle<ReadHandle>>(HandleWrapper{std::move(stdinRead), std::move(closeStdin)}, stream.get()));
1601 +
1602 + fds.emplace(WSLCFDStdin, TypedHandle{wil::unique_handle{stdinWrite.release()}, WSLCHandleTypePipe});
1603 + }
1604 + else
1605 + {
1606 + // If stdin is not attached, close it now to make sure no one tries to write to it.
1607 + closeStdin();
1608 + }
1609 +
1610 + auto [stdoutRead, stdoutWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
1611 + auto [stderrRead, stderrWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
1612 +
1613 + fds.emplace(WSLCFDStdout, TypedHandle{wil::unique_handle{stdoutRead.release()}, WSLCHandleTypePipe});
1614 + fds.emplace(WSLCFDStderr, TypedHandle{wil::unique_handle{stderrRead.release()}, WSLCHandleTypePipe});
1615 +
1616 + ioHandles.emplace_back(std::make_unique<DockerIORelayHandle>(
1617 + std::move(stream), std::move(stdoutWrite), std::move(stderrWrite), common::relay::DockerIORelayHandle::Format::Raw));
1618 +
1619 + m_ioRelay.AddHandles(std::move(ioHandles));
1620 +
1621 + return std::make_unique<RelayedProcessIO>(std::move(fds));
1622 +}
1623 +
1624 +void WSLCContainerImpl::MapPorts()
1625 +{
1626 + std::map<uint16_t, std::shared_ptr<VmPortAllocation>> allocatedPorts;
1627 +
1628 + for (auto& e : m_mappedPorts)
1629 + {
1630 + // VmPort is empty when the container is using host mode.
1631 + // In that case, allocate the VM ports to match the container ports.
1632 + if (!e.VmMapping.VmPort)
1633 + {
1634 + // Reuse existing vm port allocation when possible.
1635 + // This is required because the same container can be bind the port number for different families or protocols.
1636 + auto existing = allocatedPorts.find(e.ContainerPort);
1637 + if (existing != allocatedPorts.end())
1638 + {
1639 + e.VmMapping.AssignVmPort(existing->second);
1640 + }
1641 + else
1642 + {
1643 + auto allocatedPort =
1644 + m_virtualMachine.TryAllocatePort(e.ContainerPort, e.VmMapping.BindAddress.si_family, e.VmMapping.Protocol);
1645 +
1646 + THROW_HR_WITH_USER_ERROR_IF(
1647 + HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS),
1648 + wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id),
1649 + !allocatedPort);
1650 +
1651 + e.VmMapping.AssignVmPort(allocatedPort);
1652 +
1653 + allocatedPorts.emplace(e.ContainerPort, allocatedPort);
1654 + }
1655 + }
1656 +
1657 + try
1658 + {
1659 + m_virtualMachine.MapPort(e.VmMapping);
1660 + }
1661 + catch (...)
1662 + {
1663 + auto result = wil::ResultFromCaughtException();
1664 + if (result == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) || result == HRESULT_FROM_WIN32(WSAEADDRINUSE))
1665 + {
1666 + THROW_HR_WITH_USER_ERROR(
1667 + HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id));
1668 + }
1669 + throw;
1670 + }
1671 + }
1672 +}
1673 +
1674 +void WSLCContainerImpl::UnmapPorts()
1675 +{
1676 + for (auto& e : m_mappedPorts)
1677 + {
1678 + try
1679 + {
1680 + e.VmMapping.Unmap();
1681 + }
1682 + CATCH_LOG();
1683 +
1684 + try
1685 + {
1686 + if (m_networkingMode == WSLCContainerNetworkTypeHost)
1687 + {
1688 + e.VmMapping.VmPort.reset();
1689 + }
1690 + }
1691 + CATCH_LOG();
1692 + }
1693 +}
1694 +
1695 +__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseProcesses()
1696 +{
1697 + std::lock_guard processesLock{m_processesLock};
1698 +
1699 + // Notify all processes that the container has exited.
1700 + // The exec callback isn't always sent to execed processes, so do this to avoid 'stuck' processes.
1701 + for (auto& process : m_processes)
1702 + {
1703 + process->OnContainerReleased();
1704 + }
1705 +
1706 + m_processes.clear();
1707 +}
1708 +
1709 +__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseRuntimeResources()
1710 +{
1711 + WSL_LOG("ReleaseRuntimeResources", TraceLoggingValue(m_id.c_str(), "ID"));
1712 +
1713 + // Release runtime resources (port relays, volume mounts) that were set up at Start().
1714 + UnmapPorts();
1715 + UnmountVolumes(m_mountedVolumes, m_virtualMachine);
1716 +}
1717 +
1718 +__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::ReleaseResources()
1719 +{
1720 + WSL_LOG("ReleaseResources", TraceLoggingValue(m_id.c_str(), "ID"));
1721 +
1722 + ReleaseRuntimeResources();
1723 +
1724 + // Release VM port allocations back to the pool.
1725 + for (auto& e : m_mappedPorts)
1726 + {
1727 + e.VmMapping.VmPort.reset();
1728 + }
1729 +
1730 + return PrepareDisconnectComWrapper();
1731 +}
1732 +
1733 +__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::PrepareDisconnectComWrapper()
1734 +{
1735 + if (m_comWrapper)
1736 + {
1737 + // Cache read-only properties in the COM wrapper before disconnecting,
1738 + // so callers can still query state/process after the impl is gone.
1739 + {
1740 + std::lock_guard processesLock{m_processesLock};
1741 + m_comWrapper->CacheState(m_id, m_name, m_state, m_initProcess);
1742 + }
1743 + }
1744 +
1745 + return unique_com_disconnect{std::exchange(m_comWrapper, nullptr)};
1746 +}
1747 +
1748 +__requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerState State, std::optional<std::uint64_t> stateChangedAt) noexcept
1749 +{
1750 + // N.B. A deleted container cannot transition back to any other state.
1751 + WI_ASSERT(m_state != WslcContainerStateDeleted);
1752 +
1753 + WSL_LOG(
1754 + "ContainerStateChange",
1755 + TraceLoggingValue(static_cast<int>(m_state), "PreviousState"),
1756 + TraceLoggingValue(static_cast<int>(State), "NewState"),
1757 + TraceLoggingValue(m_id.c_str(), "ID"));
1758 +
1759 + m_state = State;
1760 + m_stateChangedAt = stateChangedAt.value_or(static_cast<std::uint64_t>(std::time(nullptr)));
1761 +}
1762 +
1763 +WSLCContainer::WSLCContainer(WSLCContainerImpl* impl, std::function<void(const WSLCContainerImpl*)>&& OnDeleted) :
1764 + COMImplClass<WSLCContainerImpl>(impl), m_onDeleted(std::move(OnDeleted))
1765 +{
1766 +}
1767 +
1768 +HRESULT WSLCContainer::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr)
1769 +{
1770 + COMServiceExecutionContext context;
1771 +
1772 + *Stdin = {};
1773 + *Stdout = {};
1774 + *Stderr = {};
1775 +
1776 + return CallImpl(&WSLCContainerImpl::Attach, DetachKeys, Stdin, Stdout, Stderr);
1777 +}
1778 +
1779 +HRESULT WSLCContainer::GetState(WSLCContainerState* Result)
1780 +{
1781 + COMServiceExecutionContext context;
1782 + RETURN_HR_IF_NULL(E_POINTER, Result);
1783 +
1784 + *Result = WslcContainerStateInvalid;
1785 + HRESULT hr = CallImpl(&WSLCContainerImpl::GetState, Result);
1786 + if (SUCCEEDED(hr))
1787 + {
1788 + return S_OK;
1789 + }
1790 +
1791 + // PrepareDisconnectComWrapper() populates the cache before setting m_impl to null,
1792 + // so if CallImpl failed with RPC_E_DISCONNECTED, the cache must be populated.
1793 + if (hr == RPC_E_DISCONNECTED)
1794 + {
1795 + auto cacheLock = m_cacheLock.lock_shared();
1796 + if (WI_VERIFY(m_cachedState.has_value()))
1797 + {
1798 + *Result = m_cachedState.value();
1799 + return S_OK;
1800 + }
1801 + }
1802 +
1803 + return hr;
1804 +}
1805 +
1806 +HRESULT WSLCContainer::GetInitProcess(IWSLCProcess** Process)
1807 +{
1808 + COMServiceExecutionContext context;
1809 +
1810 + *Process = nullptr;
1811 +
1812 + HRESULT hr = CallImpl(&WSLCContainerImpl::GetInitProcess, Process);
1813 + if (SUCCEEDED(hr))
1814 + {
1815 + return S_OK;
1816 + }
1817 +
1818 + // PrepareDisconnectComWrapper() populates the cache before setting m_impl to null,
1819 + // so if CallImpl failed with RPC_E_DISCONNECTED, the cache must be populated.
1820 + if (hr == RPC_E_DISCONNECTED)
1821 + {
1822 + auto cacheLock = m_cacheLock.lock_shared();
1823 + if (m_cachedInitProcess)
1824 + {
1825 + return m_cachedInitProcess.CopyTo(__uuidof(IWSLCProcess), (void**)Process);
1826 + }
1827 + }
1828 +
1829 + return hr;
1830 +}
1831 +
1832 +HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKeys, IWSLCProcess** Process)
1833 +{
1834 + COMServiceExecutionContext context;
1835 +
1836 + *Process = nullptr;
1837 + return CallImpl(&WSLCContainerImpl::Exec, Options, DetachKeys, Process);
1838 +}
1839 +
1840 +HRESULT WSLCContainer::Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds)
1841 +{
1842 + COMServiceExecutionContext context;
1843 +
1844 + return CallImpl(&WSLCContainerImpl::Stop, Signal, TimeoutSeconds, false);
1845 +}
1846 +
1847 +HRESULT WSLCContainer::Kill(_In_ WSLCSignal Signal)
1848 +{
1849 + COMServiceExecutionContext context;
1850 +
1851 + return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true);
1852 +}
1853 +
1854 +HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys)
1855 +try
1856 +{
1857 + COMServiceExecutionContext context;
1858 +
1859 + THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags);
1860 +
1861 + return CallImpl(&WSLCContainerImpl::Start, Flags, DetachKeys);
1862 +}
1863 +CATCH_RETURN();
1864 +
1865 +HRESULT WSLCContainer::Inspect(LPSTR* Output)
1866 +{
1867 + COMServiceExecutionContext context;
1868 +
1869 + *Output = nullptr;
1870 +
1871 + return CallImpl(&WSLCContainerImpl::Inspect, Output);
1872 +}
1873 +
1874 +HRESULT WSLCContainer::Delete(WSLCDeleteFlags Flags)
1875 +try
1876 +{
1877 + COMServiceExecutionContext context;
1878 +
1879 + THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCDeleteFlagsValid), "Invalid flags: 0x%x", Flags);
1880 +
1881 + // Special case for Delete(): If deletion is successful, notify the WSLCSession that the container has been deleted.
1882 + auto [lock, impl] = LockImpl();
1883 +
1884 + impl->Delete(Flags);
1885 + m_onDeleted(impl);
1886 +
1887 + return S_OK;
1888 +}
1889 +CATCH_RETURN();
1890 +
1891 +void WSLCContainer::CacheState(const std::string& id, const std::string& name, WSLCContainerState state, const Microsoft::WRL::ComPtr<IWSLCProcess>& initProcess) noexcept
1892 +try
1893 +{
1894 + auto cacheLock = m_cacheLock.lock_exclusive();
1895 +
1896 + // CacheState must only be called once, during PrepareDisconnectComWrapper().
1897 + WI_ASSERT(!m_cachedState.has_value());
1898 +
1899 + m_cachedId = id;
1900 + m_cachedName = name;
1901 + m_cachedState = state;
1902 + m_cachedInitProcess = initProcess;
1903 +}
1904 +CATCH_LOG();
1905 +
1906 +HRESULT WSLCContainer::Export(WSLCHandle TarHandle)
1907 +{
1908 + COMServiceExecutionContext context;
1909 +
1910 + return CallImpl(&WSLCContainerImpl::Export, TarHandle);
1911 +}
1912 +
1913 +HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail)
1914 +try
1915 +{
1916 + COMServiceExecutionContext context;
1917 + RETURN_HR_IF(E_POINTER, Stdout == nullptr || Stderr == nullptr);
1918 +
1919 + THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCLogsFlagsValid), "Invalid flags: 0x%x", Flags);
1920 +
1921 + *Stdout = {};
1922 + *Stderr = {};
1923 +
1924 + return CallImpl(&WSLCContainerImpl::Logs, Flags, Stdout, Stderr, Since, Until, Tail);
1925 +}
1926 +CATCH_RETURN();
1927 +
1928 +HRESULT WSLCContainer::GetId(WSLCContainerId Id)
1929 +try
1930 +{
1931 + COMServiceExecutionContext context;
1932 +
1933 + const auto hr = wil::ResultFromException([&] {
1934 + auto [lock, impl] = LockImpl();
1935 + WI_VERIFY(strcpy_s(Id, std::size<char>(WSLCContainerId{}), impl->ID().c_str()) == 0);
1936 + });
1937 +
1938 + RETURN_HR_IF(hr, hr != RPC_E_DISCONNECTED);
1939 +
1940 + // PrepareDisconnectComWrapper() populates the cache before setting m_impl to null,
1941 + // so if LockImpl failed with RPC_E_DISCONNECTED, the cache must be populated.
1942 + auto cacheLock = m_cacheLock.lock_shared();
1943 + if (WI_VERIFY(m_cachedId.has_value()))
1944 + {
1945 + WI_VERIFY(strcpy_s(Id, std::size<char>(WSLCContainerId{}), m_cachedId->c_str()) == 0);
1946 + return S_OK;
1947 + }
1948 +
1949 + return hr;
1950 +}
1951 +CATCH_RETURN();
1952 +
1953 +HRESULT WSLCContainer::GetName(LPSTR* Name)
1954 +try
1955 +{
1956 + COMServiceExecutionContext context;
1957 +
1958 + RETURN_HR_IF_NULL(E_POINTER, Name);
1959 + *Name = nullptr;
1960 +
1961 + const auto hr = wil::ResultFromException([&] {
1962 + auto [lock, impl] = LockImpl();
1963 + *Name = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(impl->Name().c_str()).release();
1964 + });
1965 +
1966 + RETURN_HR_IF(hr, hr != RPC_E_DISCONNECTED);
1967 +
1968 + // PrepareDisconnectComWrapper() populates the cache before setting m_impl to null,
1969 + // so if LockImpl failed with RPC_E_DISCONNECTED, the cache must be populated.
1970 + auto cacheLock = m_cacheLock.lock_shared();
1971 + if (WI_VERIFY(m_cachedName.has_value()))
1972 + {
1973 + *Name = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(m_cachedName->c_str()).release();
1974 + return S_OK;
1975 + }
1976 +
1977 + return hr;
1978 +}
1979 +CATCH_RETURN();
1980 +
1981 +void WSLCContainerImpl::GetLabels(WSLCLabelInformation** Labels, ULONG* Count) const
1982 +{
1983 + auto lock = m_lock.lock_shared();
1984 +
1985 + if (m_labels.empty())
1986 + {
1987 + *Labels = nullptr;
1988 + *Count = 0;
1989 + return;
1990 + }
1991 +
1992 + // Build labels locally using RAII strings. If an allocation throws mid-loop,
1993 + // the vector destructor frees everything already built.
1994 + std::vector<std::pair<wil::unique_cotaskmem_ansistring, wil::unique_cotaskmem_ansistring>> localLabels;
1995 + localLabels.reserve(m_labels.size());
1996 +
1997 + for (const auto& [key, value] : m_labels)
1998 + {
1999 + localLabels.emplace_back(
2000 + wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(key.c_str()),
2001 + wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(value.c_str()));
2002 + }
2003 +
2004 + // All strings built successfully — allocate output array and transfer ownership.
2005 + auto labelsArray = wil::make_unique_cotaskmem<WSLCLabelInformation[]>(localLabels.size());
2006 + for (size_t i = 0; i < localLabels.size(); ++i)
2007 + {
2008 + labelsArray[i].Key = localLabels[i].first.release();
2009 + labelsArray[i].Value = localLabels[i].second.release();
2010 + }
2011 +
2012 + *Count = static_cast<ULONG>(localLabels.size());
2013 + *Labels = labelsArray.release();
2014 +}
2015 +
2016 +HRESULT WSLCContainer::GetLabels(WSLCLabelInformation** Labels, ULONG* Count)
2017 +try
2018 +{
2019 + COMServiceExecutionContext context;
2020 +
2021 + RETURN_HR_IF(E_POINTER, Labels == nullptr || Count == nullptr);
2022 +
2023 + *Count = 0;
2024 + *Labels = nullptr;
2025 + return CallImpl(&WSLCContainerImpl::GetLabels, Labels, Count);
2026 +}
2027 +CATCH_RETURN();
2028 +
2029 +HRESULT WSLCContainer::InterfaceSupportsErrorInfo(REFIID riid)
2030 +{
2031 + return riid == __uuidof(IWSLCContainer) ? S_OK : S_FALSE;
2032 +}
src/windows/wslcsession/WSLCContainer.h new
+244
@@ -0,0 +1,244 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCContainer.h
8 +
9 +Abstract:
10 +
11 + Contains the definition for WSLCContainer.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "ServiceProcessLauncher.h"
18 +#include "WSLCSession.h"
19 +#include "ContainerEventTracker.h"
20 +#include "DockerHTTPClient.h"
21 +#include "WSLCProcessControl.h"
22 +#include "IORelay.h"
23 +#include "COMImplClass.h"
24 +#include "wslc_schema.h"
25 +#include "WSLCContainerMetadata.h"
26 +#include "WSLCVhdVolume.h"
27 +#include <unordered_map>
28 +
29 +namespace wsl::windows::service::wslc {
30 +
31 +class WSLCContainer;
32 +class WSLCSession;
33 +
34 +class unique_com_disconnect
35 +{
36 +public:
37 + NON_COPYABLE(unique_com_disconnect);
38 + DEFAULT_MOVABLE(unique_com_disconnect);
39 +
40 + unique_com_disconnect() = default;
41 + unique_com_disconnect(Microsoft::WRL::ComPtr<WSLCContainer>&& wrapper) noexcept;
42 + ~unique_com_disconnect() noexcept;
43 +
44 +private:
45 + Microsoft::WRL::ComPtr<WSLCContainer> m_wrapper;
46 +};
47 +
48 +struct ContainerPortMapping
49 +{
50 + NON_COPYABLE(ContainerPortMapping);
51 +
52 + ContainerPortMapping(VMPortMapping&& VmMapping, uint16_t ContainerPort);
53 + ContainerPortMapping(ContainerPortMapping&& Other);
54 +
55 + ContainerPortMapping& operator=(ContainerPortMapping&& Other);
56 + const char* ProtocolString() const;
57 +
58 + WSLCPortMapping Serialize() const;
59 +
60 + VMPortMapping VmMapping;
61 + uint16_t ContainerPort{};
62 +};
63 +
64 +class WSLCContainerImpl
65 +{
66 +public:
67 + NON_COPYABLE(WSLCContainerImpl);
68 + NON_MOVABLE(WSLCContainerImpl);
69 +
70 + WSLCContainerImpl(
71 + WSLCSession& wslcSession,
72 + WSLCVirtualMachine& virtualMachine,
73 + std::string&& Id,
74 + std::string&& Name,
75 + std::string&& Image,
76 + WSLCContainerNetworkType NetworkMode,
77 + std::vector<WSLCVolumeMount>&& volumes,
78 + std::vector<ContainerPortMapping>&& ports,
79 + std::map<std::string, std::string>&& labels,
80 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
81 + ContainerEventTracker& EventTracker,
82 + DockerHTTPClient& DockerClient,
83 + IORelay& Relay,
84 + WSLCContainerState InitialState,
85 + std::uint64_t CreatedAt,
86 + WSLCProcessFlags InitProcessFlags,
87 + WSLCContainerFlags ContainerFlags);
88 +
89 + ~WSLCContainerImpl();
90 +
91 + void Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys);
92 + void Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr) const;
93 + void Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, bool Kill);
94 + void Delete(WSLCDeleteFlags Flags);
95 + void Export(WSLCHandle TarHandle) const;
96 + void GetStateChangedAt(_Out_ ULONGLONG* StateChangedAt);
97 + void GetCreatedAt(_Out_ ULONGLONG* CreatedAt);
98 + void GetState(_Out_ WSLCContainerState* State);
99 + void GetInitProcess(_Out_ IWSLCProcess** process) const;
100 + void Exec(_In_ const WSLCProcessOptions* Options, LPCSTR DetachKeys, _Out_ IWSLCProcess** Process);
101 + void Inspect(LPSTR* Output) const;
102 + void Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const;
103 + void GetLabels(WSLCLabelInformation** Labels, ULONG* Count) const;
104 +
105 + void CopyTo(IWSLCContainer** Container) const;
106 +
107 + const std::string& Image() const noexcept;
108 + const std::string& Name() const noexcept;
109 + WSLCContainerState State() const noexcept;
110 + std::vector<WSLCPortMapping> GetPorts() const;
111 +
112 + __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional<std::uint64_t> stateChangedAt = std::nullopt) noexcept;
113 +
114 + void OnProcessReleased(DockerExecProcessControl* process) noexcept;
115 +
116 + const std::string& ID() const noexcept;
117 +
118 + // Returns the container flags used to decide whether to
119 + // auto-delete the container on stop.
120 + WSLCContainerFlags Flags() const noexcept
121 + {
122 + return m_containerFlags;
123 + }
124 +
125 + static std::unique_ptr<WSLCContainerImpl> Create(
126 + const WSLCContainerOptions& Options,
127 + WSLCSession& wslcSession,
128 + WSLCVirtualMachine& virtualMachine,
129 + const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& SessionVolumes,
130 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
131 + ContainerEventTracker& EventTracker,
132 + DockerHTTPClient& DockerClient,
133 + IORelay& Relay);
134 +
135 + static std::unique_ptr<WSLCContainerImpl> Open(
136 + const common::docker_schema::ContainerInfo& DockerContainer,
137 + WSLCSession& wslcSession,
138 + WSLCVirtualMachine& virtualMachine,
139 + const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
140 + const std::unordered_set<std::string>& anonymousVolumes,
141 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
142 + ContainerEventTracker& EventTracker,
143 + DockerHTTPClient& DockerClient,
144 + IORelay& Relay);
145 +
146 +private:
147 + __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect DeleteExclusiveLockHeld(WSLCDeleteFlags Flags);
148 +
149 + void AllocateBridgedModePorts();
150 + void OnEvent(ContainerEvent event, std::optional<int> exitCode, std::uint64_t eventTime);
151 +
152 + bool WaitForEvent(const wil::unique_event& Event, std::chrono::milliseconds Timeout) const;
153 +
154 + __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect ReleaseResources();
155 + __requires_exclusive_lock_held(m_lock) void ReleaseRuntimeResources();
156 + __requires_exclusive_lock_held(m_lock) void ReleaseProcesses();
157 + __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect PrepareDisconnectComWrapper();
158 +
159 + std::unique_ptr<RelayedProcessIO> CreateRelayedProcessIO(wil::unique_handle&& stream, WSLCProcessFlags flags);
160 +
161 + wsl::windows::common::wslc_schema::InspectContainer BuildInspectContainer(const wsl::windows::common::docker_schema::InspectContainer& dockerInspect) const;
162 +
163 + void MapPorts();
164 + void UnmapPorts();
165 +
166 + mutable wil::srwlock m_lock;
167 + std::string m_name;
168 + std::string m_image;
169 + std::string m_id;
170 + WSLCProcessFlags m_initProcessFlags{};
171 + WSLCContainerFlags m_containerFlags{};
172 + mutable std::mutex m_processesLock;
173 + __guarded_by(m_processesLock) std::vector<DockerExecProcessControl*> m_processes;
174 + __guarded_by(m_processesLock) Microsoft::WRL::ComPtr<IWSLCProcess> m_initProcess;
175 + __guarded_by(m_processesLock) DockerContainerProcessControl* m_initProcessControl = nullptr;
176 +
177 + struct StopNotification
178 + {
179 + std::atomic<std::uint64_t> EventTime{0};
180 + wil::unique_event Event{wil::EventOptions::None};
181 + } m_stopNotification;
182 +
183 + // Serializes Stop() callers and signals OnEvent that a Stop is in flight.
184 + // Must be acquired before m_lock when both are needed.
185 + std::mutex m_stopLock;
186 +
187 + DockerHTTPClient& m_dockerClient;
188 + std::uint64_t m_stateChangedAt{static_cast<std::uint64_t>(std::time(nullptr))};
189 + std::uint64_t m_createdAt{};
190 + WSLCContainerState m_state = WslcContainerStateInvalid;
191 + WSLCSession& m_wslcSession;
192 + WSLCVirtualMachine& m_virtualMachine;
193 + std::vector<ContainerPortMapping> m_mappedPorts;
194 + std::vector<WSLCVolumeMount> m_mountedVolumes;
195 + std::map<std::string, std::string> m_labels;
196 + Microsoft::WRL::ComPtr<WSLCContainer> m_comWrapper;
197 + ContainerEventTracker& m_eventTracker;
198 + ContainerEventTracker::ContainerTrackingReference m_containerEvents;
199 + IORelay& m_ioRelay;
200 + WSLCContainerNetworkType m_networkingMode{};
201 +};
202 +
203 +class DECLSPEC_UUID("B1F1C4E3-C225-4CAE-AD8A-34C004DE1AE4") WSLCContainer
204 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCContainer, IFastRundown, ISupportErrorInfo>,
205 + public COMImplClass<WSLCContainerImpl>
206 +{
207 +
208 +public:
209 + WSLCContainer(WSLCContainerImpl* impl, std::function<void(const WSLCContainerImpl*)>&& OnDeleted);
210 +
211 + IFACEMETHOD(Attach)(_In_opt_ LPCSTR DetachKeys, _Out_ WSLCHandle* Stdin, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr) override;
212 + IFACEMETHOD(Stop)(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds) override;
213 + IFACEMETHOD(Kill)(_In_ WSLCSignal Signal) override;
214 + IFACEMETHOD(Delete)(WSLCDeleteFlags Flags) override;
215 + IFACEMETHOD(Export)(_In_ WSLCHandle TarHandle) override;
216 + IFACEMETHOD(GetState)(_Out_ WSLCContainerState* State) override;
217 + IFACEMETHOD(GetInitProcess)(_Out_ IWSLCProcess** process) override;
218 + IFACEMETHOD(Exec)(_In_ const WSLCProcessOptions* Options, _In_opt_ LPCSTR DetachKeys, _Out_ IWSLCProcess** Process) override;
219 + IFACEMETHOD(Start)(WSLCContainerStartFlags Flags, _In_opt_ LPCSTR DetachKeys) override;
220 + IFACEMETHOD(Inspect)(_Out_ LPSTR* Output) override;
221 + IFACEMETHOD(Logs)(_In_ WSLCLogsFlags Flags, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr, _In_ ULONGLONG Since, _In_ ULONGLONG Until, _In_ ULONGLONG Tail) override;
222 + IFACEMETHOD(GetId)(_Out_ WSLCContainerId Id) override;
223 + IFACEMETHOD(GetName)(_Out_ LPSTR* Name) override;
224 + IFACEMETHOD(GetLabels)(_Out_ WSLCLabelInformation** Labels, _Out_ ULONG* Count) override;
225 +
226 + IFACEMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);
227 +
228 + // Cache read-only properties so they remain accessible after the impl is disconnected.
229 + // Called from WSLCContainerImpl::PrepareDisconnectComWrapper() while m_lock is held exclusively.
230 + void CacheState(const std::string& id, const std::string& name, WSLCContainerState state, const Microsoft::WRL::ComPtr<IWSLCProcess>& initProcess) noexcept;
231 +
232 +private:
233 + std::function<void(const WSLCContainerImpl*)> m_onDeleted;
234 +
235 + // Cached read-only properties populated by CacheState() so they remain
236 + // accessible after the impl is disconnected.
237 + mutable wil::srwlock m_cacheLock;
238 + _Guarded_by_(m_cacheLock) std::optional<std::string> m_cachedId;
239 + _Guarded_by_(m_cacheLock) std::optional<std::string> m_cachedName;
240 + _Guarded_by_(m_cacheLock) std::optional<WSLCContainerState> m_cachedState;
241 + _Guarded_by_(m_cacheLock) Microsoft::WRL::ComPtr<IWSLCProcess> m_cachedInitProcess;
242 +};
243 +
244 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCContainerMetadata.h new
+70
@@ -0,0 +1,70 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCContainerMetadata.h
8 +
9 +Abstract:
10 +
11 + JSON schema for WSLC container metadata stored in Docker container labels.
12 + This metadata allows WSLC to recover container state across service restarts.
13 +
14 +--*/
15 +
16 +#pragma once
17 +
18 +#include "JsonUtils.h"
19 +#include "wslc.h"
20 +
21 +namespace wsl::windows::service::wslc {
22 +
23 +// Label key used to store WSLC container metadata in Docker container labels.
24 +constexpr auto WSLCContainerMetadataLabel = "com.microsoft.wsl.container.metadata";
25 +
26 +struct WSLCPortMapping
27 +{
28 + uint16_t HostPort{};
29 + uint16_t VmPort{};
30 + uint16_t ContainerPort{};
31 + int Family{};
32 + int Protocol{};
33 + std::string BindingAddress;
34 +
35 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(WSLCPortMapping, HostPort, VmPort, ContainerPort, Family, Protocol, BindingAddress);
36 +};
37 +
38 +struct WSLCVolumeMount
39 +{
40 + std::wstring HostPath;
41 + std::string ParentVMPath;
42 + std::string ContainerPath;
43 + bool ReadOnly{};
44 +
45 + // Non-empty when the mount target is a single file rather than a directory.
46 + std::wstring SourceFilename;
47 +
48 + // Runtime-only field. Not serialized to JSON.
49 + bool Mounted{};
50 +
51 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(WSLCVolumeMount, HostPath, ParentVMPath, ContainerPath, ReadOnly, SourceFilename);
52 +};
53 +
54 +struct WSLCContainerMetadataV1
55 +{
56 + WSLCContainerFlags Flags{WSLCContainerFlagsNone};
57 + WSLCProcessFlags InitProcessFlags{WSLCProcessFlagsNone};
58 + std::vector<WSLCPortMapping> Ports;
59 + std::vector<WSLCVolumeMount> Volumes;
60 +
61 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(WSLCContainerMetadataV1, Flags, InitProcessFlags, Ports, Volumes);
62 +};
63 +
64 +struct WSLCContainerMetadata
65 +{
66 + std::optional<WSLCContainerMetadataV1> V1;
67 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(WSLCContainerMetadata, V1);
68 +};
69 +
70 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/WSLCGuestVolume.cpp new
+163
@@ -0,0 +1,163 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCGuestVolume.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of WSLCGuestVolumeImpl - a WSLC volume whose storage is
12 + owned entirely by docker's built-in "local" driver inside the guest VM.
13 +
14 +--*/
15 +
16 +#include "precomp.h"
17 +#include "DockerHTTPClient.h"
18 +#include "WSLCGuestVolume.h"
19 +#include "WSLCVolumeMetadata.h"
20 +#include "wslc_schema.h"
21 +
22 +using namespace wsl::windows::common;
23 +using wsl::shared::Localization;
24 +
25 +namespace wsl::windows::service::wslc {
26 +
27 +namespace {
28 +
29 + // The Docker local driver passes type/device/o directly to mount(8).
30 + // Validate that the driver opts are either empty or only contain "type=tmpfs".
31 + void ValidateDriverOpts(const std::map<std::string, std::string>& DriverOpts)
32 + {
33 + if (DriverOpts.empty())
34 + {
35 + return;
36 + }
37 +
38 + // Determine the mount type. Only tmpfs is supported; everything else
39 + // (none, nfs, cifs, ext4, ...) requires a device path or network access.
40 + auto typeIt = DriverOpts.find("type");
41 + std::string type = (typeIt != DriverOpts.end()) ? typeIt->second : "";
42 +
43 + THROW_HR_WITH_USER_ERROR_IF(
44 + E_INVALIDARG, Localization::MessageWslcUnsupportedVolumeDriverOpts("type=" + type), !type.empty() && type != "tmpfs");
45 + }
46 +
47 +} // namespace
48 +
49 +WSLCGuestVolumeImpl::WSLCGuestVolumeImpl(
50 + std::string&& Name,
51 + std::string&& CreatedAt,
52 + std::map<std::string, std::string>&& DriverOpts,
53 + std::map<std::string, std::string>&& Labels,
54 + DockerHTTPClient& DockerClient) :
55 + m_name(std::move(Name)), m_createdAt(std::move(CreatedAt)), m_driverOpts(std::move(DriverOpts)), m_labels(std::move(Labels)), m_dockerClient(DockerClient)
56 +{
57 +}
58 +
59 +std::unique_ptr<WSLCGuestVolumeImpl> WSLCGuestVolumeImpl::Create(
60 + LPCSTR Name, std::map<std::string, std::string>&& DriverOpts, std::map<std::string, std::string>&& Labels, DockerHTTPClient& DockerClient)
61 +{
62 + ValidateDriverOpts(DriverOpts);
63 +
64 + WSLCVolumeMetadata metadata;
65 + metadata.Driver = WSLCGuestVolumeDriver;
66 + metadata.DriverOpts = DriverOpts;
67 +
68 + docker_schema::CreateVolume request{};
69 + if (Name != nullptr && Name[0] != '\0')
70 + {
71 + request.Name = Name;
72 + }
73 + request.Driver = "local";
74 + request.DriverOpts = DriverOpts;
75 + request.Labels = {{WSLCVolumeMetadataLabel, wsl::shared::ToJson(metadata)}};
76 +
77 + // Merge user labels into the Docker volume labels.
78 + for (const auto& [key, value] : Labels)
79 + {
80 + request.Labels[key] = value;
81 + }
82 +
83 + try
84 + {
85 + auto createdVolume = DockerClient.CreateVolume(request);
86 +
87 + return std::make_unique<WSLCGuestVolumeImpl>(
88 + std::move(createdVolume.Name), std::move(createdVolume.CreatedAt), std::move(DriverOpts), std::move(Labels), DockerClient);
89 + }
90 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to create volume '%hs'", Name != nullptr ? Name : "");
91 +}
92 +
93 +std::unique_ptr<WSLCGuestVolumeImpl> WSLCGuestVolumeImpl::Open(const wsl::windows::common::docker_schema::Volume& Volume, DockerHTTPClient& DockerClient)
94 +{
95 + THROW_HR_IF(E_INVALIDARG, !Volume.Labels.has_value());
96 +
97 + auto metadataIt = Volume.Labels->find(WSLCVolumeMetadataLabel);
98 + THROW_HR_IF(E_INVALIDARG, metadataIt == Volume.Labels->end());
99 +
100 + auto metadata = wsl::shared::FromJson<WSLCVolumeMetadata>(metadataIt->second.c_str());
101 + THROW_HR_IF(E_INVALIDARG, metadata.Driver != WSLCGuestVolumeDriver);
102 +
103 + THROW_HR_IF(E_INVALIDARG, Volume.Driver != "local");
104 +
105 + if (Volume.Options.has_value())
106 + {
107 + ValidateDriverOpts(Volume.Options.value());
108 + }
109 +
110 + // Extract user labels (all labels except our internal metadata label).
111 + std::map<std::string, std::string> userLabels;
112 + for (const auto& [key, value] : *Volume.Labels)
113 + {
114 + if (key != WSLCVolumeMetadataLabel)
115 + {
116 + userLabels[key] = value;
117 + }
118 + }
119 +
120 + auto volume = std::make_unique<WSLCGuestVolumeImpl>(
121 + std::string{Volume.Name}, std::string{Volume.CreatedAt}, std::move(metadata.DriverOpts), std::move(userLabels), DockerClient);
122 +
123 + return volume;
124 +}
125 +
126 +void WSLCGuestVolumeImpl::Delete()
127 +{
128 + try
129 + {
130 + m_dockerClient.RemoveVolume(m_name);
131 + }
132 + catch (const DockerHTTPException& e)
133 + {
134 + THROW_HR_WITH_USER_ERROR_IF(
135 + HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), Localization::MessageWslcVolumeInUse(m_name.c_str()), e.StatusCode() == 409);
136 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(m_name.c_str()), e.StatusCode() == 404);
137 + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to delete volume '%hs'", m_name.c_str());
138 + }
139 +}
140 +
141 +std::string WSLCGuestVolumeImpl::Inspect() const
142 +{
143 + wslc_schema::InspectVolume inspect{};
144 + inspect.Name = m_name;
145 + inspect.Driver = WSLCGuestVolumeDriver;
146 + inspect.CreatedAt = m_createdAt;
147 + inspect.DriverOpts = m_driverOpts;
148 + inspect.Labels = m_labels;
149 +
150 + return wsl::shared::ToJson(inspect);
151 +}
152 +
153 +WSLCVolumeInformation WSLCGuestVolumeImpl::GetVolumeInformation() const
154 +{
155 + WSLCVolumeInformation Info{};
156 +
157 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(Info.Name, m_name.c_str()) != 0);
158 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(Info.Driver, WSLCGuestVolumeDriver) != 0);
159 +
160 + return Info;
161 +}
162 +
163 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCGuestVolume.h new
+79
@@ -0,0 +1,79 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCGuestVolume.h
8 +
9 +Abstract:
10 +
11 + Volume implementation that delegates storage to docker's built-in "local"
12 + volume driver. The volume lives on the session storage VHD at
13 + /var/lib/docker/volumes/<name>/_data inside the guest VM. No host-side
14 + artifacts (no extra VHD file, no disk attach).
15 +
16 +--*/
17 +
18 +#pragma once
19 +
20 +#include "IWSLCVolume.h"
21 +#include "WSLCVolumeMetadata.h"
22 +#include "wslc.h"
23 +#include <map>
24 +#include <memory>
25 +#include <string>
26 +
27 +namespace wsl::windows::common::docker_schema {
28 +struct Volume;
29 +}
30 +
31 +namespace wsl::windows::service::wslc {
32 +
33 +class DockerHTTPClient;
34 +
35 +class WSLCGuestVolumeImpl : public IWSLCVolume
36 +{
37 +public:
38 + NON_COPYABLE(WSLCGuestVolumeImpl);
39 + NON_MOVABLE(WSLCGuestVolumeImpl);
40 +
41 + WSLCGuestVolumeImpl(
42 + std::string&& Name,
43 + std::string&& CreatedAt,
44 + std::map<std::string, std::string>&& DriverOpts,
45 + std::map<std::string, std::string>&& Labels,
46 + DockerHTTPClient& DockerClient);
47 +
48 + ~WSLCGuestVolumeImpl() = default;
49 +
50 + static std::unique_ptr<WSLCGuestVolumeImpl> Create(
51 + _In_opt_ LPCSTR Name,
52 + _In_ std::map<std::string, std::string>&& DriverOpts,
53 + _In_ std::map<std::string, std::string>&& Labels,
54 + _In_ DockerHTTPClient& DockerClient);
55 +
56 + static std::unique_ptr<WSLCGuestVolumeImpl> Open(_In_ const wsl::windows::common::docker_schema::Volume& Volume, _In_ DockerHTTPClient& DockerClient);
57 +
58 + // IWSLCVolume
59 + const std::string& Name() const noexcept override
60 + {
61 + return m_name;
62 + }
63 + const char* Driver() const noexcept override
64 + {
65 + return WSLCGuestVolumeDriver;
66 + }
67 + void Delete() override;
68 + std::string Inspect() const override;
69 + WSLCVolumeInformation GetVolumeInformation() const override;
70 +
71 +private:
72 + std::string m_name;
73 + std::string m_createdAt;
74 + std::map<std::string, std::string> m_driverOpts;
75 + std::map<std::string, std::string> m_labels;
76 + DockerHTTPClient& m_dockerClient;
77 +};
78 +
79 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCNetworkMetadata.h new
+51
@@ -0,0 +1,51 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCNetworkMetadata.h
8 +
9 +Abstract:
10 +
11 + Constants and types for WSLC-managed Docker networks.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +namespace wsl::windows::service::wslc {
18 +
19 +// Label key used to identify WSLC-managed Docker networks.
20 +constexpr auto WSLCNetworkManagedLabel = "com.microsoft.wsl.network.managed";
21 +constexpr auto WSLCBridgeNetworkDriver = "bridge";
22 +
23 +// Reserved Docker network names that cannot be used for custom networks.
24 +inline bool IsReservedNetworkName(const std::string& name)
25 +{
26 + return name == "bridge" || name == "host" || name == "none";
27 +}
28 +
29 +struct NetworkIPAMConfig
30 +{
31 + std::string Subnet;
32 + std::string Gateway;
33 +};
34 +
35 +struct NetworkIPAM
36 +{
37 + std::string Driver;
38 + std::optional<std::vector<NetworkIPAMConfig>> Config;
39 +};
40 +
41 +struct NetworkEntry
42 +{
43 + std::string Id;
44 + std::string Driver;
45 + std::string Scope;
46 + bool Internal{false};
47 + std::map<std::string, std::string> Labels;
48 + NetworkIPAM IPAM;
49 +};
50 +
51 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCProcess.cpp new
+115
@@ -0,0 +1,115 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCProcess.cpp
8 +
9 +Abstract:
10 +
11 + Contains the implementation of WSLCProcess.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "WSLCProcess.h"
17 +#include "WSLCVirtualMachine.h"
18 +
19 +using wsl::windows::service::wslc::WSLCProcess;
20 +
21 +WSLCProcess::WSLCProcess(std::shared_ptr<WSLCProcessControl> Control, std::unique_ptr<WSLCProcessIO>&& Io, WSLCProcessFlags Flags) :
22 + m_control(std::move(Control)), m_io(std::move(Io)), m_flags(Flags)
23 +{
24 +}
25 +
26 +HRESULT WSLCProcess::Signal(int Signal)
27 +try
28 +{
29 + m_control->Signal(Signal);
30 + return S_OK;
31 +}
32 +CATCH_RETURN();
33 +
34 +HRESULT WSLCProcess::GetExitEvent(HANDLE* Event)
35 +try
36 +{
37 + *Event = wsl::windows::common::wslutil::DuplicateHandle(m_control->GetExitEvent().get(), SYNCHRONIZE, FALSE);
38 + return S_OK;
39 +}
40 +CATCH_RETURN();
41 +
42 +HRESULT WSLCProcess::GetStdHandle(WSLCFD Fd, WSLCHandle* Handle)
43 +try
44 +{
45 + RETURN_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), !m_io, "Process IO not attached");
46 +
47 + auto typedHandle = m_io->OpenFd(Fd);
48 +
49 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !typedHandle.is_valid());
50 +
51 + DWORD Access = SYNCHRONIZE;
52 +
53 + WI_SetFlagIf(Access, GENERIC_WRITE, Fd == WSLCFDTty || Fd == WSLCFDStdin);
54 + WI_SetFlagIf(Access, GENERIC_READ, Fd == WSLCFDStdout || Fd == WSLCFDStderr || Fd == WSLCFDTty);
55 +
56 + *Handle = common::wslutil::ToCOMOutputHandle(typedHandle.get(), Access, typedHandle.Type);
57 +
58 + WSL_LOG(
59 + "GetStdHandle",
60 + TraceLoggingValue(static_cast<int>(Fd), "fd"),
61 + TraceLoggingValue(typedHandle.get(), "handle"),
62 + TraceLoggingValue(static_cast<int>(Handle->Type), "type"));
63 +
64 + return S_OK;
65 +}
66 +CATCH_RETURN();
67 +
68 +HRESULT WSLCProcess::GetFlags(WSLCProcessFlags* Flags)
69 +try
70 +{
71 + *Flags = m_flags;
72 + return S_OK;
73 +}
74 +CATCH_RETURN();
75 +
76 +wil::unique_handle WSLCProcess::GetStdHandle(int Index)
77 +{
78 + THROW_WIN32_IF(ERROR_INVALID_STATE, !m_io);
79 +
80 + return std::move(m_io->OpenFd(Index).Handle);
81 +}
82 +
83 +HANDLE WSLCProcess::GetExitEvent()
84 +{
85 + return m_control->GetExitEvent().get();
86 +}
87 +
88 +HRESULT WSLCProcess::GetPid(int* Pid)
89 +try
90 +{
91 + *Pid = m_control->GetPid();
92 + return S_OK;
93 +}
94 +CATCH_RETURN();
95 +
96 +int WSLCProcess::GetPid() const
97 +{
98 + return m_control->GetPid();
99 +}
100 +
101 +HRESULT WSLCProcess::GetState(WSLCProcessState* State, int* Code)
102 +try
103 +{
104 + std::tie(*State, *Code) = m_control->GetState();
105 + return S_OK;
106 +}
107 +CATCH_RETURN();
108 +
109 +HRESULT WSLCProcess::ResizeTty(ULONG Rows, ULONG Columns)
110 +try
111 +{
112 + m_control->ResizeTty(Rows, Columns);
113 + return S_OK;
114 +}
115 +CATCH_RETURN();
\ No newline at end of file
src/windows/wslcsession/WSLCProcess.h new
+49
@@ -0,0 +1,49 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCProcess.h
8 +
9 +Abstract:
10 +
11 + Contains the definition for WSLCProcess
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include "wslc.h"
17 +#include "WSLCProcessControl.h"
18 +#include "WSLCProcessIO.h"
19 +
20 +namespace wsl::windows::service::wslc {
21 +
22 +class WSLCVirtualMachine;
23 +
24 +class DECLSPEC_UUID("AFBEA6D6-D8A4-4F81-8FED-F947EB74B33B") WSLCProcess
25 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCProcess, IFastRundown>
26 +{
27 +public:
28 + WSLCProcess(std::shared_ptr<WSLCProcessControl> Control, std::unique_ptr<WSLCProcessIO>&& Io, WSLCProcessFlags Flags);
29 + WSLCProcess(const WSLCProcess&) = delete;
30 + WSLCProcess& operator=(const WSLCProcess&) = delete;
31 +
32 + IFACEMETHOD(Signal)(_In_ int Signal) override;
33 + IFACEMETHOD(GetExitEvent)(_Out_ HANDLE* Event) override;
34 + IFACEMETHOD(GetStdHandle)(_In_ WSLCFD Fd, _Out_ WSLCHandle* Handle) override;
35 + IFACEMETHOD(GetFlags)(_Out_ WSLCProcessFlags* Flags) override;
36 + IFACEMETHOD(GetPid)(_Out_ int* Pid) override;
37 + IFACEMETHOD(GetState)(_Out_ WSLCProcessState* State, _Out_ int* Code) override;
38 + IFACEMETHOD(ResizeTty)(_In_ ULONG Rows, _In_ ULONG Columns) override;
39 +
40 + wil::unique_handle GetStdHandle(int Index);
41 + HANDLE GetExitEvent();
42 + int GetPid() const;
43 +
44 +private:
45 + WSLCProcessFlags m_flags;
46 + std::shared_ptr<WSLCProcessControl> m_control;
47 + std::unique_ptr<WSLCProcessIO> m_io;
48 +};
49 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/WSLCProcessControl.cpp new
+274
@@ -0,0 +1,274 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCProcessControl.cpp
8 +
9 +Abstract:
10 +
11 + Contains the different WSLCProcessControl definitions for process control logic.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +
17 +#include "WSLCProcessControl.h"
18 +#include "WSLCVirtualMachine.h"
19 +#include "WSLCContainer.h"
20 +
21 +using wsl::windows::service::wslc::DockerContainerProcessControl;
22 +using wsl::windows::service::wslc::DockerExecProcessControl;
23 +using wsl::windows::service::wslc::VMProcessControl;
24 +using wsl::windows::service::wslc::WSLCProcessControl;
25 +
26 +std::pair<WSLCProcessState, int> WSLCProcessControl::GetState() const
27 +{
28 + if (m_exitEvent.is_signaled())
29 + {
30 + WI_ASSERT(m_exitedCode.has_value());
31 + return {WslcProcessStateExited, m_exitedCode.value()};
32 + }
33 + else
34 + {
35 + return {WslcProcessStateRunning, -1};
36 + }
37 +}
38 +
39 +const wil::unique_event& WSLCProcessControl::GetExitEvent() const
40 +{
41 + return m_exitEvent;
42 +}
43 +
44 +DockerContainerProcessControl::DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker) :
45 + m_container(&Container),
46 + m_client(DockerClient),
47 + m_trackingReference(EventTracker.RegisterContainerStateUpdates(
48 + Container.ID(),
49 + std::bind(&DockerContainerProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)))
50 +{
51 +}
52 +
53 +DockerContainerProcessControl::~DockerContainerProcessControl()
54 +{
55 +}
56 +
57 +void DockerContainerProcessControl::Signal(int Signal)
58 +{
59 + std::lock_guard lock{m_lock};
60 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_container == nullptr || m_exitEvent.is_signaled());
61 +
62 + m_client.SignalContainer(m_container->ID(), static_cast<WSLCSignal>(Signal));
63 +}
64 +
65 +void DockerContainerProcessControl::ResizeTty(ULONG Rows, ULONG Columns)
66 +{
67 + std::lock_guard lock{m_lock};
68 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_container == nullptr || m_exitEvent.is_signaled());
69 +
70 + m_client.ResizeContainerTty(m_container->ID(), Rows, Columns);
71 +}
72 +
73 +void DockerContainerProcessControl::OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::uint64_t /*eventTime*/)
74 +{
75 + if (Event == ContainerEvent::Stop)
76 + {
77 + std::lock_guard lock{m_lock};
78 + if (!m_exitEvent.is_signaled())
79 + {
80 + WSL_LOG("ContainerProcessStop");
81 + WI_ASSERT(ExitCode.has_value());
82 + WI_ASSERT(!m_exitedCode.has_value());
83 + m_exitedCode = ExitCode.value();
84 + m_exitEvent.SetEvent();
85 + }
86 + }
87 +}
88 +
89 +int DockerContainerProcessControl::GetPid() const
90 +{
91 + return 1;
92 +}
93 +
94 +void DockerContainerProcessControl::OnContainerReleased() noexcept
95 +{
96 + {
97 + std::lock_guard lock{m_lock};
98 +
99 + WI_ASSERT(m_container != nullptr);
100 + m_container = nullptr;
101 + }
102 +
103 + // N.B. The caller might keep a reference to the process even after the container is released.
104 + // If that happens, make sure that the state tracking can't outlive the session.
105 + // This is safe to call without the lock because removing the tracking reference is protected by the event tracker lock.
106 + m_trackingReference.Reset();
107 +
108 + // Signal the exit event to prevent callers from being blocked on it.
109 + if (!m_exitEvent.is_signaled())
110 + {
111 + m_exitedCode = 128 + WSLCSignalSIGKILL;
112 + m_exitEvent.SetEvent();
113 + }
114 +}
115 +
116 +DockerExecProcessControl::DockerExecProcessControl(
117 + WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker) :
118 + m_container(&Container),
119 + m_id(Id),
120 + m_client(DockerClient),
121 + m_trackingReference(EventTracker.RegisterExecStateUpdates(
122 + Container.ID(), Id, std::bind(&DockerExecProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)))
123 +{
124 +}
125 +
126 +DockerExecProcessControl::~DockerExecProcessControl()
127 +{
128 + std::lock_guard lock{m_lock};
129 + if (m_container != nullptr)
130 + {
131 + m_container->OnProcessReleased(this);
132 + }
133 +}
134 +
135 +int DockerExecProcessControl::GetPid() const
136 +{
137 + std::lock_guard lock{m_lock};
138 +
139 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_pid.has_value());
140 +
141 + return m_pid.value();
142 +}
143 +
144 +void DockerExecProcessControl::Signal(int Signal)
145 +{
146 + THROW_WIN32(ERROR_NOT_SUPPORTED);
147 +}
148 +
149 +void DockerExecProcessControl::ResizeTty(ULONG Rows, ULONG Columns)
150 +{
151 + std::lock_guard lock{m_lock};
152 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_container == nullptr || m_exitEvent.is_signaled());
153 +
154 + m_client.ResizeExecTty(m_id, Rows, Columns);
155 +}
156 +
157 +void DockerExecProcessControl::SetPid(int Pid)
158 +{
159 + std::lock_guard lock{m_lock};
160 +
161 + WI_ASSERT(!m_pid.has_value());
162 +
163 + m_pid = Pid;
164 +}
165 +
166 +void DockerExecProcessControl::SetExitCode(int ExitCode)
167 +{
168 + std::lock_guard lock{m_lock};
169 +
170 + if (!m_exitedCode.has_value())
171 + {
172 + m_exitedCode = ExitCode;
173 + m_exitEvent.SetEvent();
174 + }
175 +}
176 +
177 +void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::uint64_t /*eventTime*/)
178 +{
179 + if (Event == ContainerEvent::ExecDied && !m_exitEvent.is_signaled())
180 + {
181 + WI_ASSERT(ExitCode.has_value());
182 +
183 + SetExitCode(ExitCode.value());
184 + }
185 +}
186 +
187 +void DockerExecProcessControl::OnContainerReleased() noexcept
188 +{
189 + {
190 + std::lock_guard lock{m_lock};
191 +
192 + WI_ASSERT(m_container != nullptr);
193 + m_container = nullptr;
194 + }
195 +
196 + // N.B. The caller might keep a reference to the process even after the container is released.
197 + // If that happens, make sure that the state tracking can't outlive the session.
198 + // This is safe to call without the lock because removing the tracking reference is protected by the event tracker lock.
199 +
200 + m_trackingReference.Reset();
201 +
202 + // Signal the exit event to prevent callers being blocked on it.
203 + if (!m_exitEvent.is_signaled())
204 + {
205 + m_exitedCode = 128 + WSLCSignalSIGKILL;
206 + m_exitEvent.SetEvent();
207 + }
208 +}
209 +
210 +VMProcessControl::VMProcessControl(WSLCVirtualMachine& VirtualMachine, int Pid, wil::unique_socket&& TtyControl) :
211 + m_pid(Pid), m_ttyControlChannel(std::move(TtyControl), "TtyControl", VirtualMachine.TerminatingEvent()), m_vm(&VirtualMachine)
212 +{
213 +}
214 +
215 +VMProcessControl::~VMProcessControl()
216 +{
217 + std::lock_guard lock{m_lock};
218 +
219 + if (m_vm != nullptr)
220 + {
221 + m_vm->OnProcessReleased(m_pid);
222 + }
223 +}
224 +
225 +void VMProcessControl::Signal(int Signal)
226 +{
227 + std::lock_guard lock{m_lock};
228 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_vm == nullptr || m_exitEvent.is_signaled());
229 +
230 + m_vm->Signal(m_pid, Signal);
231 +}
232 +
233 +void VMProcessControl::ResizeTty(ULONG Rows, ULONG Columns)
234 +{
235 + std::lock_guard lock{m_lock};
236 +
237 + THROW_WIN32_IF(ERROR_INVALID_STATE, !m_ttyControlChannel.Connected());
238 + THROW_HR_IF(E_INVALIDARG, Rows == 0 || Columns == 0 || Rows > USHORT_MAX || Columns > USHORT_MAX);
239 +
240 + WSLC_TERMINAL_CHANGED message{};
241 + message.Rows = static_cast<unsigned short>(Rows);
242 + message.Columns = static_cast<unsigned short>(Columns);
243 + m_ttyControlChannel.SendMessage(message);
244 +}
245 +
246 +void VMProcessControl::OnExited(int Code)
247 +{
248 + std::lock_guard lock{m_lock};
249 +
250 + if (!m_exitEvent.is_signaled())
251 + {
252 + m_exitedCode = Code;
253 + m_ttyControlChannel.Close();
254 + m_exitEvent.SetEvent();
255 + }
256 +}
257 +
258 +int VMProcessControl::GetPid() const
259 +{
260 + return m_pid;
261 +}
262 +
263 +void VMProcessControl::OnVmTerminated()
264 +{
265 + std::lock_guard lock{m_lock};
266 + m_vm = nullptr;
267 +
268 + // Make sure that the process is in a terminated state, so users don't think that it might still be running.
269 + if (!m_exitEvent.is_signaled())
270 + {
271 + m_exitedCode = 128 + WSLCSignalSIGKILL;
272 + m_exitEvent.SetEvent();
273 + }
274 +}
\ No newline at end of file
src/windows/wslcsession/WSLCProcessControl.h new
+105
@@ -0,0 +1,105 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCProcessControl.h
8 +
9 +Abstract:
10 +
11 + Contains the different WSLCProcessControl definitions for process control logic.
12 +
13 +--*/
14 +
15 +#pragma once
16 +#include "wslc.h"
17 +#include "DockerHTTPClient.h"
18 +#include "ContainerEventTracker.h"
19 +
20 +namespace wsl::windows::service::wslc {
21 +
22 +class WSLCVirtualMachine;
23 +class WSLCContainerImpl;
24 +
25 +class WSLCProcessControl
26 +{
27 +public:
28 + WSLCProcessControl() = default;
29 + virtual ~WSLCProcessControl() = default;
30 +
31 + virtual void Signal(int Signal) = 0;
32 + virtual void ResizeTty(ULONG Rows, ULONG Columns) = 0;
33 + virtual int GetPid() const = 0;
34 + std::pair<WSLCProcessState, int> GetState() const;
35 + const wil::unique_event& GetExitEvent() const;
36 +
37 +protected:
38 + wil::unique_event m_exitEvent{wil::EventOptions::ManualReset};
39 + std::optional<int> m_exitedCode{};
40 +};
41 +
42 +class DockerContainerProcessControl : public WSLCProcessControl
43 +{
44 +public:
45 + DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker);
46 + ~DockerContainerProcessControl();
47 + void Signal(int Signal) override;
48 + void ResizeTty(ULONG Rows, ULONG Columns) override;
49 + int GetPid() const override;
50 + void OnContainerReleased() noexcept;
51 +
52 +private:
53 + void OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::uint64_t eventTime);
54 +
55 + std::mutex m_lock;
56 + DockerHTTPClient& m_client;
57 + WSLCContainerImpl* m_container{};
58 + ContainerEventTracker::ContainerTrackingReference m_trackingReference;
59 +};
60 +
61 +class DockerExecProcessControl : public WSLCProcessControl
62 +{
63 +public:
64 + DockerExecProcessControl(WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker);
65 + ~DockerExecProcessControl();
66 + void Signal(int Signal) override;
67 + void ResizeTty(ULONG Rows, ULONG Columns) override;
68 + int GetPid() const override;
69 + void OnContainerReleased() noexcept;
70 +
71 + void SetPid(int Pid);
72 + void SetExitCode(int ExitCode);
73 +
74 +private:
75 + void OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::uint64_t eventTime);
76 +
77 + mutable std::mutex m_lock;
78 + std::string m_id;
79 + std::optional<int> m_pid{};
80 + DockerHTTPClient& m_client;
81 + WSLCContainerImpl* m_container{};
82 + ContainerEventTracker::ContainerTrackingReference m_trackingReference;
83 +};
84 +
85 +class VMProcessControl : public WSLCProcessControl
86 +{
87 +public:
88 + VMProcessControl(WSLCVirtualMachine& VirtualMachine, int Pid, wil::unique_socket&& TtyControl);
89 + ~VMProcessControl();
90 +
91 + void Signal(int Signal) override;
92 + void ResizeTty(ULONG Rows, ULONG Columns) override;
93 + int GetPid() const override;
94 +
95 + void OnExited(int Code);
96 + void OnVmTerminated();
97 +
98 +private:
99 + std::mutex m_lock;
100 + int m_pid{};
101 + wsl::shared::SocketChannel m_ttyControlChannel;
102 + WSLCVirtualMachine* m_vm{};
103 +};
104 +
105 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/WSLCProcessIO.cpp new
+60
@@ -0,0 +1,60 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCProcessIO.cpp
8 +
9 +Abstract:
10 +
11 + Contains the different WSLCProcessIO implementations for process IO handling.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +
17 +#include "WSLCProcessIO.h"
18 +
19 +using wsl::windows::service::wslc::RelayedProcessIO;
20 +using wsl::windows::service::wslc::TTYProcessIO;
21 +using wsl::windows::service::wslc::TypedHandle;
22 +using wsl::windows::service::wslc::VMProcessIO;
23 +using namespace wsl::windows::common::relay;
24 +
25 +RelayedProcessIO::RelayedProcessIO(std::map<ULONG, TypedHandle>&& fds) : m_relayedHandles(std::move(fds))
26 +{
27 +}
28 +
29 +TypedHandle RelayedProcessIO::OpenFd(ULONG Fd)
30 +{
31 + auto it = m_relayedHandles.find(Fd);
32 +
33 + THROW_HR_IF_MSG(E_INVALIDARG, it == m_relayedHandles.end(), "Fd not found in relayed handles: %i", static_cast<int>(Fd));
34 + THROW_WIN32_IF_MSG(ERROR_INVALID_STATE, !it->second.is_valid(), "Fd already consumed: %i", static_cast<int>(Fd));
35 +
36 + return std::move(it->second);
37 +}
38 +
39 +TTYProcessIO::TTYProcessIO(TypedHandle&& IoStream) : m_ioStream(std::move(IoStream))
40 +{
41 +}
42 +
43 +TypedHandle TTYProcessIO::OpenFd(ULONG Fd)
44 +{
45 + THROW_HR_IF_MSG(E_INVALIDARG, Fd != WSLCFDTty, "Invalid fd type for TTY process: %i", static_cast<int>(Fd));
46 +
47 + return std::move(m_ioStream);
48 +}
49 +
50 +VMProcessIO::VMProcessIO(std::map<ULONG, TypedHandle>&& handles) : m_handles(std::move(handles))
51 +{
52 +}
53 +
54 +TypedHandle VMProcessIO::OpenFd(ULONG Fd)
55 +{
56 + auto it = m_handles.find(Fd);
57 + THROW_HR_IF_MSG(E_INVALIDARG, it == m_handles.end(), "Invalid fd type for VM process: %i", static_cast<int>(Fd));
58 +
59 + return std::move(it->second);
60 +}
\ No newline at end of file
src/windows/wslcsession/WSLCProcessIO.h new
+79
@@ -0,0 +1,79 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCProcessIO.h
8 +
9 +Abstract:
10 +
11 + Contains the different WSLCProcessIO definitions for process IO handling.
12 +
13 +--*/
14 +
15 +#pragma once
16 +#include "wslc.h"
17 +
18 +namespace wsl::windows::service::wslc {
19 +
20 +struct TypedHandle
21 +{
22 + wil::unique_handle Handle;
23 + WSLCHandleType Type = WSLCHandleTypeUnknown;
24 +
25 + TypedHandle() = default;
26 + TypedHandle(wil::unique_handle&& handle, WSLCHandleType type) : Handle(std::move(handle)), Type(type)
27 + {
28 + }
29 +
30 + bool is_valid() const noexcept
31 + {
32 + return Handle.is_valid();
33 + }
34 + HANDLE get() const noexcept
35 + {
36 + return Handle.get();
37 + }
38 +};
39 +
40 +class WSLCProcessIO
41 +{
42 +public:
43 + virtual ~WSLCProcessIO() = default;
44 + virtual TypedHandle OpenFd(ULONG Fd) = 0;
45 +};
46 +
47 +class RelayedProcessIO : public WSLCProcessIO
48 +{
49 +public:
50 + RelayedProcessIO(std::map<ULONG, TypedHandle>&& fds);
51 +
52 + TypedHandle OpenFd(ULONG Fd) override;
53 +
54 +private:
55 + std::map<ULONG, TypedHandle> m_relayedHandles;
56 +};
57 +
58 +class TTYProcessIO : public WSLCProcessIO
59 +{
60 +public:
61 + TTYProcessIO(TypedHandle&& IoStream);
62 +
63 + TypedHandle OpenFd(ULONG Fd) override;
64 +
65 +private:
66 + TypedHandle m_ioStream;
67 +};
68 +
69 +class VMProcessIO : public WSLCProcessIO
70 +{
71 +public:
72 + VMProcessIO(std::map<ULONG, TypedHandle>&& handles);
73 + TypedHandle OpenFd(ULONG Fd) override;
74 +
75 +private:
76 + std::map<ULONG, TypedHandle> m_handles;
77 +};
78 +
79 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/WSLCSession.cpp new
+2785
@@ -0,0 +1,2785 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSession.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the implementation of the WSLCSession COM class.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "WSLCSession.h"
17 +#include "WSLCContainer.h"
18 +#include "WSLCNetworkMetadata.h"
19 +#include "ServiceProcessLauncher.h"
20 +#include "WslCoreFilesystem.h"
21 +
22 +using namespace wsl::windows::common;
23 +using relay::MultiHandleWait;
24 +using wsl::shared::Localization;
25 +using wsl::windows::service::wslc::UserCOMCallback;
26 +using wsl::windows::service::wslc::UserHandle;
27 +using wsl::windows::service::wslc::WSLCSession;
28 +using wsl::windows::service::wslc::WSLCVirtualMachine;
29 +
30 +constexpr auto c_containerdStorage = "/var/lib/docker";
31 +constexpr auto c_containerdSocket = "/run/containerd/containerd.sock";
32 +constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000;
33 +constexpr DWORD c_processKillTimeoutMs = 10 * 1000;
34 +
35 +namespace {
36 +
37 +std::string IndentLines(const std::string& input, const std::string& prefix)
38 +{
39 + if (input.empty())
40 + {
41 + return {};
42 + }
43 +
44 + std::string result = prefix;
45 + for (size_t i = 0; i < input.size(); i++)
46 + {
47 + result.push_back(input[i]);
48 + if (i + 1 < input.size())
49 + {
50 + if (input[i] == '\n' || (input[i] == '\r' && input[i + 1] != '\n'))
51 + {
52 + result.append(prefix);
53 + }
54 + }
55 + }
56 +
57 + return result;
58 +}
59 +
60 +void ValidateName(LPCSTR Name, size_t maxLength)
61 +{
62 + const auto& locale = std::locale::classic();
63 + size_t i = 0;
64 +
65 + for (; Name[i] != '\0'; i++)
66 + {
67 + if (!std::isalnum(Name[i], locale) && Name[i] != '_' && Name[i] != '-' && Name[i] != '.')
68 + {
69 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcInvalidName(Name));
70 + }
71 + }
72 +
73 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidName(Name), i == 0 || i > maxLength);
74 +}
75 +
76 +wslc_schema::InspectImage ConvertInspectImage(const docker_schema::InspectImage& dockerInspect)
77 +{
78 + wslc_schema::InspectImage wslcInspect{};
79 +
80 + // Direct field mappings
81 + wslcInspect.Id = dockerInspect.Id;
82 + wslcInspect.RepoTags = dockerInspect.RepoTags;
83 + wslcInspect.RepoDigests = dockerInspect.RepoDigests;
84 + wslcInspect.Parent = dockerInspect.Parent;
85 + wslcInspect.Comment = dockerInspect.Comment;
86 + wslcInspect.Created = dockerInspect.Created;
87 + wslcInspect.Author = dockerInspect.Author;
88 + wslcInspect.Architecture = dockerInspect.Architecture;
89 + wslcInspect.Os = dockerInspect.Os;
90 + wslcInspect.Size = dockerInspect.Size;
91 + wslcInspect.Metadata = dockerInspect.Metadata;
92 +
93 + // Convert Config from docker_schema to wslc_schema
94 + if (dockerInspect.Config.has_value())
95 + {
96 + wslc_schema::ImageConfig wslcConfig{};
97 + const auto& dockerConfig = dockerInspect.Config.value();
98 +
99 + wslcConfig.Cmd = dockerConfig.Cmd;
100 + wslcConfig.Entrypoint = dockerConfig.Entrypoint;
101 + wslcConfig.Env = dockerConfig.Env;
102 + wslcConfig.Labels = dockerConfig.Labels;
103 + wslcConfig.User = dockerConfig.User;
104 + wslcConfig.WorkingDir = dockerConfig.WorkingDir;
105 +
106 + wslcInspect.Config = wslcConfig;
107 + }
108 +
109 + return wslcInspect;
110 +}
111 +
112 +} // namespace
113 +
114 +namespace wsl::windows::service::wslc {
115 +
116 +UserHandle::UserHandle(WSLCSession& Session, HANDLE handle) : m_session(&Session), m_handle(handle)
117 +{
118 + WI_ASSERT(!!m_handle);
119 +}
120 +
121 +UserHandle::UserHandle(UserHandle&& Other)
122 +{
123 + *this = std::move(Other);
124 +}
125 +
126 +UserHandle& UserHandle::operator=(UserHandle&& Other)
127 +{
128 + if (this != &Other)
129 + {
130 + Reset();
131 + m_session = Other.m_session;
132 + m_handle = Other.m_handle;
133 +
134 + Other.m_handle = nullptr;
135 + Other.m_session = nullptr;
136 + }
137 + return *this;
138 +}
139 +
140 +void UserHandle::Reset()
141 +{
142 + if (m_handle != nullptr)
143 + {
144 + WI_ASSERT(m_session != nullptr);
145 +
146 + m_session->ReleaseUserHandle(m_handle);
147 + m_handle = nullptr;
148 + }
149 +}
150 +
151 +UserHandle::~UserHandle()
152 +{
153 + Reset();
154 +}
155 +
156 +HANDLE UserHandle::Get() const noexcept
157 +{
158 + return m_handle;
159 +}
160 +
161 +UserCOMCallback::UserCOMCallback(WSLCSession& Session) noexcept : m_session(&Session), m_threadId(GetCurrentThreadId())
162 +{
163 +}
164 +
165 +UserCOMCallback::UserCOMCallback(UserCOMCallback&& Other) noexcept
166 +{
167 + *this = std::move(Other);
168 +}
169 +
170 +UserCOMCallback& UserCOMCallback::operator=(UserCOMCallback&& Other) noexcept
171 +{
172 + if (this != &Other)
173 + {
174 + Reset();
175 + m_session = Other.m_session;
176 + m_threadId = Other.m_threadId;
177 +
178 + Other.m_threadId = 0;
179 + Other.m_session = nullptr;
180 + }
181 + return *this;
182 +}
183 +
184 +void UserCOMCallback::Reset() noexcept
185 +{
186 + if (m_threadId != 0)
187 + {
188 + WI_ASSERT(m_session != nullptr);
189 +
190 + m_session->UnregisterUserCOMCallback(m_threadId);
191 + m_threadId = 0;
192 +
193 + LOG_IF_FAILED(CoDisableCallCancellation(nullptr));
194 + }
195 +}
196 +
197 +UserCOMCallback::~UserCOMCallback() noexcept
198 +{
199 + Reset();
200 +}
201 +
202 +HRESULT WSLCSession::GetProcessHandle(_Out_ HANDLE* ProcessHandle)
203 +try
204 +{
205 + RETURN_HR_IF(E_POINTER, ProcessHandle == nullptr);
206 +
207 + wil::unique_handle process{OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, FALSE, GetCurrentProcessId())};
208 + THROW_LAST_ERROR_IF(!process);
209 +
210 + *ProcessHandle = process.release();
211 + return S_OK;
212 +}
213 +CATCH_RETURN();
214 +
215 +HRESULT WSLCSession::Initialize(_In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm)
216 +try
217 +{
218 + RETURN_HR_IF(E_POINTER, Settings == nullptr || Vm == nullptr);
219 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_virtualMachine.has_value());
220 +
221 + // N.B. No locking is required because Initialize() is always called before the session is returned to the caller.
222 + m_id = Settings->SessionId;
223 + m_displayName = Settings->DisplayName ? Settings->DisplayName : L"";
224 + m_featureFlags = Settings->FeatureFlags;
225 +
226 + // Get user token for the current process
227 + const auto tokenInfo = wil::get_token_information<TOKEN_USER>(GetCurrentProcessToken());
228 +
229 + WSL_LOG(
230 + "SessionInitialized",
231 + TraceLoggingValue(m_id, "SessionId"),
232 + TraceLoggingValue(m_displayName.c_str(), "DisplayName"),
233 + TraceLoggingValue(Settings->CreatorPid, "CreatorPid"));
234 +
235 + // Create the VM.
236 + m_virtualMachine.emplace(Vm, Settings);
237 +
238 + // Make sure that everything is destroyed correctly if an exception is thrown.
239 + auto errorCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(Terminate()); });
240 +
241 + m_virtualMachine->Initialize();
242 +
243 + // Get an event from the service that is signaled when the VM exits.
244 + THROW_IF_FAILED(Vm->GetTerminationEvent(&m_vmExitedEvent));
245 +
246 + // Configure storage.
247 + ConfigureStorage(*Settings, tokenInfo->User.Sid);
248 +
249 + // Launch containerd first
250 + StartContainerd();
251 +
252 + // Launch dockerd with external containerd socket
253 + StartDockerd();
254 +
255 + // Wait for dockerd to be ready before starting the event tracker.
256 + THROW_WIN32_IF_MSG(
257 + ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(Settings->BootTimeoutMs), "Timed out waiting for dockerd to start");
258 +
259 + auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread);
260 +
261 + m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000);
262 +
263 + // Start the event tracker.
264 + m_eventTracker.emplace(m_dockerClient.value(), m_id, m_ioRelay);
265 +
266 + // Monitor for unexpected VM exit.
267 + m_ioRelay.AddHandle(
268 + std::make_unique<windows::common::relay::EventHandle>(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this)));
269 +
270 + // Recover any existing containers from storage.
271 + RecoverExistingNetworks();
272 + RecoverExistingVolumes();
273 + RecoverExistingContainers();
274 +
275 + errorCleanup.release();
276 + return S_OK;
277 +}
278 +CATCH_RETURN()
279 +
280 +WSLCSession::~WSLCSession()
281 +{
282 + WSL_LOG("SessionTerminated", TraceLoggingValue(m_id, "SessionId"), TraceLoggingValue(m_displayName.c_str(), "DisplayName"));
283 +
284 + LOG_IF_FAILED(Terminate());
285 +
286 + if (m_destructionCallback)
287 + {
288 + m_destructionCallback();
289 + }
290 +}
291 +
292 +void WSLCSession::SetDestructionCallback(std::function<void()>&& callback)
293 +{
294 + m_destructionCallback = std::move(callback);
295 +}
296 +
297 +void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid)
298 +{
299 + if (Settings.StoragePath == nullptr)
300 + {
301 + // If no storage path is specified, use a tmpfs for convenience.
302 + m_virtualMachine->Mount("", c_containerdStorage, "tmpfs", "", 0);
303 + return;
304 + }
305 +
306 + std::filesystem::path storagePath{Settings.StoragePath};
307 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Settings.StoragePath), !storagePath.is_absolute());
308 +
309 + m_storageVhdPath = storagePath / "storage.vhdx";
310 +
311 + std::string diskDevice;
312 + std::optional<ULONG> diskLun{};
313 + bool vhdCreated = false;
314 +
315 + auto deleteVhdOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
316 + if (vhdCreated)
317 + {
318 + if (diskLun.has_value())
319 + {
320 + m_virtualMachine->DetachDisk(diskLun.value());
321 + }
322 +
323 + LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_storageVhdPath.c_str()));
324 + }
325 + });
326 +
327 + auto result =
328 + wil::ResultFromException([&]() { diskDevice = m_virtualMachine->AttachDisk(m_storageVhdPath.c_str(), false).second; });
329 +
330 + if (FAILED(result))
331 + {
332 + THROW_HR_IF_MSG(
333 + result,
334 + result != HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) && result != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND),
335 + "Failed to attach vhd: %ls",
336 + m_storageVhdPath.c_str());
337 +
338 + THROW_HR_WITH_USER_ERROR_IF(
339 + HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND),
340 + Localization::MessageWslcSessionStorageNotFound(Settings.StoragePath),
341 + WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsNoCreate));
342 +
343 + // If the VHD wasn't found, create it.
344 + WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath"));
345 +
346 + std::filesystem::create_directories(storagePath);
347 + wsl::core::filesystem::CreateVhd(m_storageVhdPath.c_str(), Settings.MaximumStorageSizeMb * _1MB, UserSid, false, false);
348 + vhdCreated = true;
349 +
350 + // Then attach the new disk.
351 + std::tie(diskLun, diskDevice) = m_virtualMachine->AttachDisk(m_storageVhdPath.c_str(), false);
352 +
353 + // Then format it.
354 + m_virtualMachine->Ext4Format(diskDevice);
355 + }
356 +
357 + // Mount the device to /root.
358 + m_virtualMachine->Mount(diskDevice.c_str(), c_containerdStorage, "ext4", "", 0);
359 +
360 + deleteVhdOnFailure.release();
361 +}
362 +
363 +HRESULT WSLCSession::GetId(ULONG* Id)
364 +{
365 + *Id = m_id;
366 +
367 + return S_OK;
368 +}
369 +
370 +void WSLCSession::OnDockerdExited()
371 +{
372 + if (!m_sessionTerminatingEvent.is_signaled())
373 + {
374 + WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name"));
375 + }
376 +}
377 +
378 +void WSLCSession::OnContainerdExited()
379 +{
380 + if (!m_sessionTerminatingEvent.is_signaled())
381 + {
382 + WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name"));
383 + }
384 +}
385 +
386 +void WSLCSession::OnVmExited()
387 +{
388 + WSL_LOG(
389 + "VmExited",
390 + TraceLoggingLevel(WINEVENT_LEVEL_WARNING),
391 + TraceLoggingValue(m_id, "SessionId"),
392 + TraceLoggingValue(m_displayName.c_str(), "Name"),
393 + TraceLoggingValue(!m_sessionTerminatingEvent.is_signaled(), "Unexpected"));
394 +
395 + LOG_IF_FAILED(Terminate());
396 +}
397 +
398 +void WSLCSession::OnProcessLog(const gsl::span<char>& Buffer, PCSTR Source)
399 +try
400 +{
401 + if (Buffer.empty())
402 + {
403 + return;
404 + }
405 +
406 + constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock";
407 +
408 + std::string entry = {Buffer.begin(), Buffer.end()};
409 + WSL_LOG(
410 + "ContainerdLog",
411 + TraceLoggingValue(Source, "Source"),
412 + TraceLoggingValue(entry.c_str(), "Content"),
413 + TraceLoggingValue(m_displayName.c_str(), "Name"));
414 +
415 + if (!m_dockerdReadyEvent.is_signaled())
416 + {
417 + if (entry.find(c_dockerdReadyLogLine) != std::string::npos)
418 + {
419 + m_dockerdReadyEvent.SetEvent();
420 + }
421 + }
422 +}
423 +CATCH_LOG();
424 +
425 +ServiceRunningProcess WSLCSession::StartProcess(
426 + const std::string& Executable, const std::vector<std::string>& Args, PCSTR LogSource, std::function<void()>&& ExitCallback)
427 +{
428 + ServiceProcessLauncher launcher{Executable, Args, {{"PATH=/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin"}}};
429 +
430 + auto process = launcher.Launch(*m_virtualMachine);
431 +
432 + m_ioRelay.AddHandle(std::make_unique<windows::common::relay::LineBasedReadHandle>(
433 + process.GetStdHandle(1), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false));
434 +
435 + m_ioRelay.AddHandle(std::make_unique<windows::common::relay::LineBasedReadHandle>(
436 + process.GetStdHandle(2), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false));
437 +
438 + m_ioRelay.AddHandle(std::make_unique<windows::common::relay::EventHandle>(process.GetExitEvent(), std::move(ExitCallback)));
439 +
440 + return process;
441 +}
442 +
443 +void WSLCSession::StartContainerd()
444 +{
445 + constexpr auto c_containerdRoot = "/var/lib/docker/containerd/daemon";
446 + constexpr auto c_containerdState = "/run/docker/containerd/daemon";
447 +
448 + std::vector<std::string> args{"/usr/bin/containerd", "--address", c_containerdSocket, "--root", c_containerdRoot, "--state", c_containerdState};
449 +
450 + if (WI_IsFlagSet(m_featureFlags, WslcFeatureFlagsDebug))
451 + {
452 + args.emplace_back("--log-level");
453 + args.emplace_back("debug");
454 + }
455 +
456 + m_containerdProcess = StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this));
457 + WSL_LOG("ContainerdStarted");
458 +}
459 +
460 +void WSLCSession::StartDockerd()
461 +{
462 + std::vector<std::string> args{"/usr/bin/dockerd", "--containerd", c_containerdSocket};
463 +
464 + if (WI_IsFlagSet(m_featureFlags, WslcFeatureFlagsDebug))
465 + {
466 + args.emplace_back("--debug");
467 + }
468 +
469 + m_dockerdProcess = StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this));
470 + WSL_LOG("DockerdStarted");
471 +}
472 +
473 +void WSLCSession::StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback)
474 +{
475 + auto io = CreateIOContext();
476 +
477 + struct Response
478 + {
479 + boost::beast::http::status result;
480 + bool isJson = false;
481 + };
482 +
483 + std::optional<UserCOMCallback> comCall;
484 + if (ProgressCallback != nullptr)
485 + {
486 + comCall = RegisterUserCOMCallback();
487 + }
488 +
489 + std::optional<Response> httpResponse;
490 +
491 + auto onHttpResponse = [&](const boost::beast::http::message<false, boost::beast::http::buffer_body>& response) {
492 + WSL_LOG(
493 + "ImageOperationHttpResponse",
494 + TraceLoggingValue(OperationName, "Operation"),
495 + TraceLoggingValue(static_cast<int>(response.result()), "StatusCode"));
496 +
497 + auto it = response.find(boost::beast::http::field::content_type);
498 + httpResponse.emplace(response.result(), it != response.end() && it->value().starts_with("application/json"));
499 + };
500 +
501 + std::string errorJson;
502 + std::optional<std::string> reportedError;
503 + auto onChunk = [&](const gsl::span<char>& Content) {
504 + if (httpResponse.has_value() && httpResponse->result != boost::beast::http::status::ok)
505 + {
506 + // If the status code is an error, then this is an error message, not a progress update.
507 + errorJson.append(Content.data(), Content.size());
508 + return;
509 + }
510 +
511 + std::string contentString{Content.begin(), Content.end()};
512 + WSL_LOG(
513 + "ImageOperationProgress",
514 + TraceLoggingValue(OperationName, "Operation"),
515 + TraceLoggingValue(Image, "Image"),
516 + TraceLoggingValue(contentString.c_str(), "Content"));
517 +
518 + auto parsed = wsl::shared::FromJson<docker_schema::CreateImageProgress>(contentString.c_str());
519 +
520 + if (parsed.errorDetail.has_value())
521 + {
522 + if (reportedError.has_value())
523 + {
524 + LOG_HR_MSG(
525 + E_UNEXPECTED,
526 + "Received multiple error messages during image %hs. Previous: %hs, New: %hs",
527 + OperationName,
528 + reportedError->c_str(),
529 + parsed.errorDetail->message.c_str());
530 + }
531 +
532 + reportedError = parsed.errorDetail->message;
533 + return;
534 + }
535 +
536 + if (ProgressCallback != nullptr)
537 + {
538 + THROW_IF_FAILED(ProgressCallback->OnProgress(
539 + parsed.status.c_str(), parsed.id.c_str(), parsed.progressDetail.current, parsed.progressDetail.total));
540 + }
541 + };
542 +
543 + auto onCompleted = [&]() { io.Cancel(); };
544 +
545 + io.AddHandle(std::make_unique<DockerHTTPClient::DockerHttpResponseHandle>(
546 + requestContext, std::move(onHttpResponse), std::move(onChunk), std::move(onCompleted)));
547 +
548 + io.Run({});
549 +
550 + THROW_HR_IF(E_UNEXPECTED, !httpResponse.has_value());
551 +
552 + if (httpResponse->result != boost::beast::http::status::ok)
553 + {
554 + std::string errorMessage;
555 + if (httpResponse->isJson)
556 + {
557 + // operation failed, parse the error message.
558 + errorMessage = wsl::shared::FromJson<docker_schema::ErrorResponse>(errorJson.c_str()).message;
559 + }
560 + else
561 + {
562 + // If no error message was explicitly returned, use the response body, if any.
563 + errorMessage = errorJson;
564 + }
565 +
566 + if (httpResponse->result == boost::beast::http::status::not_found)
567 + {
568 + THROW_HR_WITH_USER_ERROR(WSLC_E_IMAGE_NOT_FOUND, errorMessage);
569 + }
570 + else if (httpResponse->result == boost::beast::http::status::bad_request)
571 + {
572 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
573 + }
574 + else
575 + {
576 + THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
577 + }
578 + }
579 + else if (reportedError.has_value())
580 + {
581 + // Can happen if an error is returned during progress after receiving an OK status.
582 + THROW_HR_WITH_USER_ERROR(E_FAIL, reportedError.value().c_str());
583 + }
584 +}
585 +
586 +HRESULT WSLCSession::PullImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback)
587 +try
588 +{
589 + COMServiceExecutionContext context;
590 +
591 + RETURN_HR_IF_NULL(E_POINTER, Image);
592 +
593 + auto lock = m_lock.lock_shared();
594 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
595 +
596 + auto [repo, tagOrDigest] = wslutil::ParseImage(Image);
597 +
598 + if (!tagOrDigest.has_value())
599 + {
600 + tagOrDigest = "latest";
601 + }
602 +
603 + std::optional<std::string> registryAuth;
604 +
605 + if (RegistryAuthenticationInformation != nullptr && *RegistryAuthenticationInformation != '\0')
606 + {
607 + registryAuth = std::string(RegistryAuthenticationInformation);
608 + }
609 +
610 + auto requestContext = m_dockerClient->PullImage(repo, tagOrDigest, registryAuth);
611 + StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback);
612 +
613 + return S_OK;
614 +}
615 +CATCH_RETURN();
616 +
617 +HRESULT WSLCSession::BuildImage(const WSLCBuildImageOptions* Options, IProgressCallback* ProgressCallback, HANDLE CancelEvent)
618 +try
619 +{
620 + COMServiceExecutionContext context;
621 +
622 + RETURN_HR_IF_NULL(E_POINTER, Options);
623 + RETURN_HR_IF_NULL(E_POINTER, Options->ContextPath);
624 + RETURN_HR_IF(E_INVALIDARG, *Options->ContextPath == L'\0');
625 + RETURN_HR_IF(E_INVALIDARG, Options->Tags.Count > 0 && Options->Tags.Values == nullptr);
626 + RETURN_HR_IF(E_INVALIDARG, Options->BuildArgs.Count > 0 && Options->BuildArgs.Values == nullptr);
627 + THROW_HR_IF_MSG(
628 + E_INVALIDARG,
629 + WI_IsAnyFlagSet(static_cast<WSLCBuildImageFlags>(Options->Flags), ~WSLCBuildImageFlagsValid),
630 + "Invalid flags: 0x%x",
631 + Options->Flags);
632 +
633 + auto buildFileHandle = OpenUserHandle(Options->DockerfileHandle);
634 +
635 + std::optional<UserCOMCallback> comCall;
636 + if (ProgressCallback != nullptr)
637 + {
638 + comCall = RegisterUserCOMCallback();
639 + }
640 +
641 + auto lock = m_lock.lock_shared();
642 +
643 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
644 +
645 + GUID volumeId{};
646 + THROW_IF_FAILED(CoCreateGuid(&volumeId));
647 + auto mountPath = std::format("/mnt/{}", wsl::shared::string::GuidToString<char>(volumeId));
648 + THROW_IF_FAILED(m_virtualMachine->MountWindowsFolder(Options->ContextPath, mountPath.c_str(), TRUE));
649 + auto unmountFolder =
650 + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { m_virtualMachine->UnmountWindowsFolder(mountPath.c_str()); });
651 +
652 + std::vector<std::string> buildArgs{"/usr/bin/docker", "build", "--progress=rawjson"};
653 + if (WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsNoCache))
654 + {
655 + buildArgs.push_back("--no-cache");
656 + }
657 + if (WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsPull))
658 + {
659 + buildArgs.push_back("--pull");
660 + }
661 + if (Options->Target != nullptr && Options->Target[0] != '\0')
662 + {
663 + buildArgs.push_back("--target");
664 + buildArgs.push_back(Options->Target);
665 + }
666 + for (ULONG i = 0; i < Options->Tags.Count; i++)
667 + {
668 + RETURN_HR_IF_NULL(E_INVALIDARG, Options->Tags.Values[i]);
669 + RETURN_HR_IF(E_INVALIDARG, strlen(Options->Tags.Values[i]) > WSLC_MAX_IMAGE_NAME_LENGTH);
670 + buildArgs.push_back("-t");
671 + buildArgs.push_back(Options->Tags.Values[i]);
672 + }
673 + for (ULONG i = 0; i < Options->BuildArgs.Count; i++)
674 + {
675 + RETURN_HR_IF_NULL(E_INVALIDARG, Options->BuildArgs.Values[i]);
676 + RETURN_HR_IF(E_INVALIDARG, Options->BuildArgs.Values[i][0] == '-');
677 + buildArgs.push_back("--build-arg");
678 + buildArgs.push_back(Options->BuildArgs.Values[i]);
679 + }
680 +
681 + buildArgs.push_back("-f");
682 + buildArgs.push_back("-");
683 + buildArgs.push_back(mountPath);
684 +
685 + WSL_LOG("BuildImageStart", TraceLoggingValue(wsl::shared::string::Join(buildArgs, ' ').c_str(), "Command"));
686 +
687 + ServiceProcessLauncher buildLauncher(buildArgs[0], buildArgs, {}, WSLCProcessFlagsStdin);
688 + auto buildProcess = buildLauncher.Launch(*m_virtualMachine);
689 +
690 + auto io = CreateIOContext();
691 +
692 + io.AddHandle(std::make_unique<relay::RelayHandle<relay::ReadHandle>>(
693 + buildFileHandle.Get(), common::relay::HandleWrapper{buildProcess.GetStdHandle(WSLCFDStdin)}));
694 +
695 + bool verbose = WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsVerbose);
696 + std::string allOutput;
697 + std::string pendingJson;
698 + std::set<std::string> reportedSteps;
699 + std::set<std::string> reportedErrors;
700 + std::map<std::string, std::string> digestToStageName;
701 + bool needsNewline = false; // true when the last log chunk didn't end with \n
702 + std::string lastLogVertex; // digest of the vertex that produced the last log output
703 +
704 + // Extract the named build stage from a BuildKit vertex name. Vertices within the same named stage
705 + // (e.g. "[builder 1/3]" and "[builder 2/3]") share a key. Returns empty for unnamed stages.
706 + auto getStageName = [](const std::string& name) -> std::string {
707 + if (name.size() < 2 || name[0] != '[')
708 + {
709 + return {};
710 + }
711 +
712 + auto close = name.find(']');
713 + if (close == std::string::npos)
714 + {
715 + return {};
716 + }
717 +
718 + // Pattern: "[name N/M]" or "[N/M]". The stage name is the part before "N/M".
719 + std::string content = name.substr(1, close - 1);
720 + auto slash = content.find('/');
721 + if (slash != std::string::npos)
722 + {
723 + auto space = content.rfind(' ', slash);
724 + if (space != std::string::npos)
725 + {
726 + return content.substr(0, space);
727 + }
728 + }
729 +
730 + return {};
731 + };
732 +
733 + auto logPrefix = [](const std::string& name) -> std::string {
734 + if (name.empty())
735 + {
736 + return " | ";
737 + }
738 + return " [" + name + "] ";
739 + };
740 +
741 + auto reportProgress = [&](const std::string& message) {
742 + if (ProgressCallback != nullptr)
743 + {
744 + THROW_IF_FAILED(ProgressCallback->OnProgress(message.c_str(), "", 0, 0));
745 + }
746 + };
747 +
748 + auto flushLine = [&]() {
749 + if (needsNewline)
750 + {
751 + reportProgress("\n");
752 + needsNewline = false;
753 + }
754 + };
755 +
756 + // Accumulate lines and use accept() to detect complete JSON objects. Check for non-JSON lines between JSON objects and add
757 + // them to the output in case they contain helpful information about the build.
758 + auto captureOutput = [&](const gsl::span<char>& content) {
759 + std::string line{content.begin(), content.end()};
760 +
761 + pendingJson.append(line);
762 +
763 + if (!nlohmann::json::accept(pendingJson))
764 + {
765 + if (pendingJson.empty() || pendingJson[0] != '{')
766 + {
767 + allOutput.append(pendingJson).append("\n");
768 + pendingJson.clear();
769 + }
770 +
771 + return;
772 + }
773 +
774 + auto json = nlohmann::json::parse(pendingJson);
775 + pendingJson.clear();
776 +
777 + docker_schema::BuildKitSolveStatus status{};
778 + from_json(json, status);
779 +
780 + // Process vertices before logs so digestToStageName is populated for log correlation.
781 + for (const auto& vertex : status.vertexes)
782 + {
783 + if (!verbose && vertex.name.find("[internal]") != std::string::npos)
784 + {
785 + continue;
786 + }
787 +
788 + digestToStageName.try_emplace(vertex.digest, getStageName(vertex.name));
789 +
790 + if (!vertex.started.empty() && reportedSteps.insert(vertex.digest).second)
791 + {
792 + flushLine();
793 + reportProgress(vertex.name + "\n");
794 + }
795 +
796 + if (!vertex.error.empty() && reportedErrors.insert(vertex.digest).second)
797 + {
798 + flushLine();
799 + reportProgress(vertex.error + "\n");
800 + }
801 + }
802 +
803 + for (const auto& log : status.logs)
804 + {
805 + if (auto it = digestToStageName.find(log.vertex); it != digestToStageName.end() && !log.data.empty())
806 + {
807 + std::string decoded = wslutil::Base64Decode(log.data);
808 + if (!decoded.empty())
809 + {
810 + if (log.vertex != lastLogVertex && decoded[0] != '\n')
811 + {
812 + flushLine();
813 + }
814 +
815 + // When continuing an unterminated line, emit the leading \n or \r directly
816 + // so it terminates/overwrites cleanly without a spurious prefix.
817 + if (needsNewline && (decoded[0] == '\n' || decoded[0] == '\r'))
818 + {
819 + reportProgress(decoded.substr(0, 1));
820 + decoded.erase(0, 1);
821 + }
822 +
823 + if (!decoded.empty())
824 + {
825 + reportProgress(IndentLines(decoded, logPrefix(it->second)));
826 + }
827 +
828 + needsNewline = !decoded.empty() && decoded.back() != '\n';
829 + lastLogVertex = log.vertex;
830 + }
831 + }
832 + }
833 +
834 + for (const auto& entry : status.statuses)
835 + {
836 + if (auto it = digestToStageName.find(entry.vertex);
837 + it != digestToStageName.end() && !entry.id.empty() && reportedSteps.insert(entry.id).second)
838 + {
839 + flushLine();
840 + reportProgress(logPrefix(it->second) + entry.id + "\n");
841 + }
842 + }
843 + };
844 +
845 + // With --progress=rawjson, docker writes progress to stderr and the final image ID to stdout on success (empty on
846 + // failure). Stdout is drained into allOutput (shown only on error) and its EOF signals build completion.
847 + io.AddHandle(
848 + std::make_unique<relay::ReadHandle>(
849 + buildProcess.GetStdHandle(1), [&](const auto& content) { allOutput.append(content.begin(), content.end()); }),
850 + relay::MultiHandleWait::CancelOnCompleted);
851 +
852 + io.AddHandle(std::make_unique<relay::LineBasedReadHandle>(buildProcess.GetStdHandle(2), captureOutput, false));
853 +
854 + // Handle cancellation within the IO loop (NeedNotComplete) so pipes keep draining.
855 + bool cancelled = false;
856 + wil::unique_handle killTimer;
857 + if (CancelEvent != nullptr)
858 + {
859 + killTimer.reset(CreateWaitableTimer(nullptr, TRUE, nullptr));
860 + THROW_LAST_ERROR_IF_NULL(killTimer);
861 +
862 + io.AddHandle(
863 + std::make_unique<relay::EventHandle>(
864 + CancelEvent,
865 + [&]() {
866 + cancelled = true;
867 + LOG_IF_FAILED(buildProcess.Get().Signal(WSLCSignalSIGTERM));
868 + LARGE_INTEGER dueTime{.QuadPart = -10LL * 10 * 1000 * 1000}; // 10 seconds
869 + THROW_IF_WIN32_BOOL_FALSE(SetWaitableTimer(killTimer.get(), &dueTime, 0, nullptr, nullptr, FALSE));
870 + }),
871 + relay::MultiHandleWait::NeedNotComplete);
872 +
873 + io.AddHandle(
874 + std::make_unique<relay::EventHandle>(
875 + killTimer.get(), [&]() { LOG_IF_FAILED(buildProcess.Get().Signal(WSLCSignalSIGKILL)); }),
876 + relay::MultiHandleWait::NeedNotComplete);
877 + }
878 +
879 + try
880 + {
881 + io.Run({});
882 + }
883 + catch (...)
884 + {
885 + flushLine();
886 + LOG_IF_FAILED(buildProcess.Get().Signal(WSLCSignalSIGTERM));
887 + try
888 + {
889 + buildProcess.Wait(10 * 1000);
890 + }
891 + catch (...)
892 + {
893 + if (wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_TIMEOUT))
894 + {
895 + LOG_IF_FAILED(buildProcess.Get().Signal(WSLCSignalSIGKILL));
896 + try
897 + {
898 + buildProcess.Wait(10 * 1000);
899 + }
900 + catch (...)
901 + {
902 + LOG_CAUGHT_EXCEPTION_MSG("Build process did not exit after SIGKILL");
903 + }
904 + }
905 + }
906 + throw;
907 + }
908 +
909 + flushLine();
910 +
911 + THROW_HR_IF_MSG(E_ABORT, cancelled, "Cancellation handle was signaled");
912 +
913 + int exitCode = buildProcess.Wait();
914 + WSL_LOG("BuildImageComplete", TraceLoggingValue(exitCode, "ExitCode"));
915 + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, allOutput, exitCode != 0);
916 +
917 + return S_OK;
918 +}
919 +CATCH_RETURN();
920 +
921 +HRESULT WSLCSession::LoadImage(const WSLCHandle ImageHandle, IProgressCallback* ProgressCallback, ULONGLONG ContentSize)
922 +try
923 +{
924 + UNREFERENCED_PARAMETER(ProgressCallback);
925 +
926 + COMServiceExecutionContext context;
927 +
928 + auto lock = m_lock.lock_shared();
929 +
930 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
931 +
932 + auto requestContext = m_dockerClient->LoadImage(ContentSize);
933 +
934 + ImportImageImpl(*requestContext, ImageHandle);
935 + return S_OK;
936 +}
937 +CATCH_RETURN();
938 +
939 +HRESULT WSLCSession::ImportImage(const WSLCHandle ImageHandle, LPCSTR ImageName, IProgressCallback* ProgressCallback, ULONGLONG ContentSize)
940 +try
941 +{
942 + UNREFERENCED_PARAMETER(ProgressCallback);
943 +
944 + COMServiceExecutionContext context;
945 +
946 + RETURN_HR_IF_NULL(E_POINTER, ImageName);
947 + RETURN_HR_IF(E_INVALIDARG, strlen(ImageName) > WSLC_MAX_IMAGE_NAME_LENGTH);
948 +
949 + auto [repo, tagOrDigest] = wslutil::ParseImage(ImageName);
950 +
951 + THROW_HR_IF_MSG(E_INVALIDARG, !tagOrDigest.has_value(), "Expected tag for image import: %hs", ImageName);
952 +
953 + auto lock = m_lock.lock_shared();
954 +
955 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
956 +
957 + auto requestContext = m_dockerClient->ImportImage(repo, tagOrDigest.value(), ContentSize);
958 +
959 + ImportImageImpl(*requestContext, ImageHandle);
960 + return S_OK;
961 +}
962 +CATCH_RETURN();
963 +
964 +void WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle)
965 +{
966 + auto userHandle = OpenUserHandle(ImageHandle);
967 +
968 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
969 +
970 + auto io = CreateIOContext();
971 +
972 + std::optional<std::string> pendingErrorJson;
973 + auto onHttpResponse = [&](const boost::beast::http::message<false, boost::beast::http::buffer_body>& response) {
974 + WSL_LOG("ImageImportHttpResponse", TraceLoggingValue(static_cast<int>(response.result()), "StatusCode"));
975 +
976 + if (response.result_int() != 200)
977 + {
978 + auto it = response.find(boost::beast::http::field::content_type);
979 +
980 + THROW_HR_IF_MSG(
981 + E_UNEXPECTED,
982 + it == response.end() || !it->value().starts_with("application/json"),
983 + "Received HTTP %i but Content-Type is not json",
984 + response.result_int());
985 +
986 + pendingErrorJson.emplace();
987 + }
988 + };
989 +
990 + std::optional<std::string> errorMessage;
991 + auto onProgress = [&](const gsl::span<char>& buffer) {
992 + if (pendingErrorJson.has_value())
993 + {
994 + // If we received a non-200 status code, then the response body is an error message. Accumulate to the error message.
995 + pendingErrorJson->append(buffer.data(), buffer.size());
996 + return;
997 + }
998 +
999 + auto parsed = shared::FromJson<docker_schema::ImageLoadResult>(std::string(buffer.begin(), buffer.end()).c_str());
1000 +
1001 + if (parsed.errorDetail.has_value())
1002 + {
1003 + if (errorMessage.has_value())
1004 + {
1005 + LOG_HR_MSG(
1006 + E_UNEXPECTED,
1007 + "Overriding previous error message '%hs' with new message '%hs'",
1008 + errorMessage->c_str(),
1009 + parsed.errorDetail->message.c_str());
1010 + }
1011 +
1012 + errorMessage = std::move(parsed.errorDetail->message);
1013 + }
1014 + else if (parsed.stream.has_value())
1015 + {
1016 + // TODO: report progress to caller.
1017 + WSL_LOG("ImageImportProgress", TraceLoggingValue(parsed.stream->c_str(), "Content"));
1018 + }
1019 + else
1020 + {
1021 + LOG_HR_MSG(E_UNEXPECTED, "Failed to parse import progress: %.*hs", static_cast<int>(buffer.size()), buffer.data());
1022 + }
1023 + };
1024 +
1025 + // Shutdown the Docker stream's write side when the user pipe is closed.
1026 + // This is required for Docker to know when the request body is complete.
1027 + auto onInputComplete = [socket = Request.stream.native_handle()]() {
1028 + LOG_LAST_ERROR_IF(shutdown(socket, SD_SEND) == SOCKET_ERROR);
1029 + };
1030 +
1031 + io.AddHandle(std::make_unique<relay::RelayHandle<relay::ReadHandle>>(
1032 + common::relay::HandleWrapper{userHandle.Get(), std::move(onInputComplete)},
1033 + common::relay::HandleWrapper{Request.stream.native_handle()}));
1034 +
1035 + io.AddHandle(
1036 + std::make_unique<DockerHTTPClient::DockerHttpResponseHandle>(Request, std::move(onHttpResponse), std::move(onProgress)),
1037 + MultiHandleWait::CancelOnCompleted);
1038 +
1039 + io.Run({});
1040 +
1041 + // Look for an error message returned as an HTTP response (non HTTP 200)
1042 + if (pendingErrorJson.has_value())
1043 + {
1044 + auto error = wsl::shared::FromJson<docker_schema::ErrorResponse>(pendingErrorJson->c_str());
1045 +
1046 + THROW_HR_WITH_USER_ERROR(E_FAIL, error.message);
1047 + }
1048 +
1049 + // Otherwise look for an error message returned via the progress stream (HTTP 200 followed by a stream error).
1050 + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, errorMessage.value(), errorMessage.has_value());
1051 +}
1052 +
1053 +HRESULT WSLCSession::SaveImage(WSLCHandle OutHandle, LPCSTR ImageNameOrID, IProgressCallback* ProgressCallback, HANDLE CancelEvent)
1054 +try
1055 +{
1056 + UNREFERENCED_PARAMETER(ProgressCallback);
1057 +
1058 + COMServiceExecutionContext context;
1059 +
1060 + RETURN_HR_IF_NULL(E_POINTER, ImageNameOrID);
1061 + RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrID) > WSLC_MAX_IMAGE_NAME_LENGTH);
1062 + auto lock = m_lock.lock_shared();
1063 +
1064 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1065 +
1066 + auto retVal = m_dockerClient->SaveImage(ImageNameOrID);
1067 + SaveImageImpl(retVal, OutHandle, CancelEvent);
1068 + return S_OK;
1069 +}
1070 +CATCH_RETURN();
1071 +
1072 +void WSLCSession::SaveImageImpl(std::pair<uint32_t, wil::unique_socket>& SocketCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent)
1073 +{
1074 + auto userHandle = OpenUserHandle(OutputHandle);
1075 +
1076 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1077 +
1078 + auto io = CreateIOContext(CancelEvent);
1079 +
1080 + std::string errorJson;
1081 +
1082 + if (SocketCodePair.first != 200)
1083 + {
1084 + auto accumulateError = [&](const gsl::span<char>& buffer) {
1085 + // If the save failed, accumulate the error message.
1086 + errorJson.append(buffer.data(), buffer.size());
1087 + };
1088 +
1089 + io.AddHandle(
1090 + std::make_unique<relay::ReadHandle>(common::relay::HandleWrapper{std::move(SocketCodePair.second)}, std::move(accumulateError)),
1091 + MultiHandleWait::CancelOnCompleted);
1092 + }
1093 + else
1094 + {
1095 + io.AddHandle(
1096 + std::make_unique<relay::RelayHandle<relay::HTTPChunkBasedReadHandle>>(
1097 + common::relay::HandleWrapper{std::move(SocketCodePair.second)}, userHandle.Get()),
1098 + MultiHandleWait::CancelOnCompleted);
1099 + }
1100 +
1101 + io.Run({});
1102 +
1103 + if (SocketCodePair.first != 200)
1104 + {
1105 + // Save failed, parse the error message.
1106 + auto error = wsl::shared::FromJson<docker_schema::ErrorResponse>(errorJson.c_str());
1107 + THROW_HR_WITH_USER_ERROR(E_FAIL, error.message.c_str());
1108 + }
1109 +}
1110 +
1111 +HRESULT WSLCSession::ListImages(const WSLCListImageOptions* Options, WSLCImageInformation** Images, ULONG* Count)
1112 +try
1113 +{
1114 + COMServiceExecutionContext context;
1115 +
1116 + RETURN_HR_IF_NULL(E_POINTER, Images);
1117 + RETURN_HR_IF_NULL(E_POINTER, Count);
1118 +
1119 + *Count = 0;
1120 + *Images = nullptr;
1121 +
1122 + if (Options != nullptr)
1123 + {
1124 + RETURN_HR_IF(E_INVALIDARG, WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDanglingTrue) && WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDanglingFalse));
1125 + RETURN_HR_IF(E_INVALIDARG, Options->LabelsCount > 0 && Options->Labels == nullptr);
1126 + RETURN_HR_IF(E_INVALIDARG, Options->Reference != nullptr && strlen(Options->Reference) > WSLC_MAX_IMAGE_NAME_LENGTH);
1127 + }
1128 +
1129 + auto lock = m_lock.lock_shared();
1130 +
1131 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1132 +
1133 + // Extract options for Docker API
1134 + bool all = false;
1135 + bool digests = false;
1136 + DockerHTTPClient::ListImagesFilters filters;
1137 +
1138 + if (Options != nullptr)
1139 + {
1140 + all = WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsAll);
1141 + digests = WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDigests);
1142 +
1143 + if (Options->Reference != nullptr)
1144 + {
1145 + filters.reference = Options->Reference;
1146 + }
1147 +
1148 + if (Options->Before != nullptr)
1149 + {
1150 + filters.before = Options->Before;
1151 + }
1152 +
1153 + if (Options->Since != nullptr)
1154 + {
1155 + filters.since = Options->Since;
1156 + }
1157 +
1158 + // Check dangling flags (mutually exclusive in practice)
1159 + if (WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDanglingTrue))
1160 + {
1161 + filters.dangling = true;
1162 + }
1163 + else if (WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDanglingFalse))
1164 + {
1165 + filters.dangling = false;
1166 + }
1167 + // If neither flag is set, filters.dangling remains std::nullopt (show all)
1168 +
1169 + // Construct labels
1170 + if (Options->Labels != nullptr && Options->LabelsCount > 0)
1171 + {
1172 + for (ULONG i = 0; i < Options->LabelsCount; ++i)
1173 + {
1174 + const auto& label = Options->Labels[i];
1175 + RETURN_HR_IF_NULL(E_POINTER, label.Key);
1176 +
1177 + std::string labelFilter = label.Key;
1178 + if (label.Value != nullptr)
1179 + {
1180 + labelFilter += "=";
1181 + labelFilter += label.Value;
1182 + }
1183 + filters.labels.push_back(labelFilter);
1184 + }
1185 + }
1186 + }
1187 +
1188 + std::vector<docker_schema::Image> images;
1189 + try
1190 + {
1191 + images = m_dockerClient->ListImages(all, digests, filters);
1192 + }
1193 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list images");
1194 +
1195 + // Compute the number of entries - one entry per tag, or one per image if no tags
1196 + auto entries = std::accumulate<decltype(images.begin()), size_t>(images.begin(), images.end(), 0, [](auto sum, const auto& e) {
1197 + return sum + (e.RepoTags.empty() ? 1 : e.RepoTags.size());
1198 + });
1199 +
1200 + auto output = wil::make_unique_cotaskmem<WSLCImageInformation[]>(entries);
1201 +
1202 + size_t index = 0;
1203 + for (const auto& e : images)
1204 + {
1205 + // Build a map from repo name to digest for this image
1206 + // RepoDigests format: "repo@sha256:digest"
1207 + std::map<std::string, std::string> repoToDigest;
1208 + for (const auto& repoDigest : e.RepoDigests)
1209 + {
1210 + size_t atPos = repoDigest.find('@');
1211 + THROW_HR_IF(E_UNEXPECTED, atPos == std::string::npos || atPos == 0);
1212 + std::string repoName = repoDigest.substr(0, atPos);
1213 + repoToDigest[repoName] = repoDigest;
1214 + }
1215 +
1216 + if (e.RepoTags.empty())
1217 + {
1218 + // Image has no tags (dangling image)
1219 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, "<none>:<none>") != 0);
1220 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Hash, e.Id.c_str()) != 0);
1221 +
1222 + // Set digest if available
1223 + if (!e.RepoDigests.empty())
1224 + {
1225 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Digest, e.RepoDigests[0].c_str()) != 0);
1226 + }
1227 + else
1228 + {
1229 + output[index].Digest[0] = '\0';
1230 + }
1231 +
1232 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].ParentId, e.ParentId.c_str()) != 0);
1233 + output[index].Size = e.Size;
1234 + output[index].Created = e.Created;
1235 + index++;
1236 + }
1237 + else
1238 + {
1239 + // Image has tags - create one entry per tag
1240 + for (const auto& tag : e.RepoTags)
1241 + {
1242 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, tag.c_str()) != 0);
1243 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Hash, e.Id.c_str()) != 0);
1244 +
1245 + // Extract repo name from tag (format: "repo:tag")
1246 + // and lookup corresponding digest from the map
1247 + auto repoName = wslutil::ParseImage(tag).first;
1248 + auto it = repoToDigest.find(repoName);
1249 + if (it != repoToDigest.end())
1250 + {
1251 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Digest, it->second.c_str()) != 0);
1252 + }
1253 + else
1254 + {
1255 + output[index].Digest[0] = '\0';
1256 + }
1257 +
1258 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].ParentId, e.ParentId.c_str()) != 0);
1259 + output[index].Size = e.Size;
1260 + output[index].Created = e.Created;
1261 + index++;
1262 + }
1263 + }
1264 + }
1265 +
1266 + WI_ASSERT(index == entries);
1267 +
1268 + *Count = static_cast<ULONG>(entries);
1269 + *Images = output.release();
1270 + return S_OK;
1271 +}
1272 +CATCH_RETURN();
1273 +
1274 +HRESULT WSLCSession::DeleteImage(const WSLCDeleteImageOptions* Options, WSLCDeletedImageInformation** DeletedImages, ULONG* Count)
1275 +try
1276 +{
1277 + COMServiceExecutionContext context;
1278 +
1279 + RETURN_HR_IF_NULL(E_POINTER, Options);
1280 + RETURN_HR_IF_NULL(E_POINTER, Options->Image);
1281 + RETURN_HR_IF(E_INVALIDARG, strlen(Options->Image) > WSLC_MAX_IMAGE_NAME_LENGTH);
1282 + THROW_HR_IF_MSG(
1283 + E_INVALIDARG,
1284 + WI_IsAnyFlagSet(static_cast<WSLCDeleteImageFlags>(Options->Flags), ~WSLCDeleteImageFlagsValid),
1285 + "Invalid flags: 0x%x",
1286 + Options->Flags);
1287 + RETURN_HR_IF_NULL(E_POINTER, DeletedImages);
1288 + RETURN_HR_IF_NULL(E_POINTER, Count);
1289 +
1290 + *DeletedImages = nullptr;
1291 + *Count = 0;
1292 +
1293 + auto lock = m_lock.lock_shared();
1294 +
1295 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1296 +
1297 + std::vector<docker_schema::DeletedImage> deletedImages;
1298 + try
1299 + {
1300 + deletedImages = m_dockerClient->DeleteImage(
1301 + Options->Image, WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsForce), WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsNoPrune));
1302 + }
1303 + catch (const DockerHTTPException& e)
1304 + {
1305 + std::string errorMessage;
1306 + if ((e.StatusCode() >= 400 && e.StatusCode() < 500))
1307 + {
1308 + errorMessage = e.DockerMessage<docker_schema::ErrorResponse>().message;
1309 + }
1310 +
1311 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_IMAGE_NOT_FOUND, errorMessage, e.StatusCode() == 404);
1312 + THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), errorMessage, e.StatusCode() == 409);
1313 + THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
1314 + }
1315 +
1316 + THROW_HR_IF_MSG(E_FAIL, deletedImages.empty(), "Failed to delete image: %hs", Options->Image);
1317 +
1318 + auto output = wil::make_unique_cotaskmem<WSLCDeletedImageInformation[]>(deletedImages.size());
1319 +
1320 + size_t index = 0;
1321 + for (const auto& image : deletedImages)
1322 + {
1323 + THROW_HR_IF(E_UNEXPECTED, (image.Deleted.empty() && image.Untagged.empty()) || (!image.Deleted.empty() && !image.Untagged.empty()));
1324 +
1325 + if (!image.Deleted.empty())
1326 + {
1327 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, image.Deleted.c_str()) != 0);
1328 + output[index].Type = WSLCDeletedImageTypeDeleted;
1329 + }
1330 + else
1331 + {
1332 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, image.Untagged.c_str()) != 0);
1333 + output[index].Type = WSLCDeletedImageTypeUntagged;
1334 + }
1335 +
1336 + index++;
1337 + }
1338 +
1339 + *Count = static_cast<ULONG>(deletedImages.size());
1340 + *DeletedImages = output.release();
1341 +
1342 + return S_OK;
1343 +}
1344 +CATCH_RETURN();
1345 +
1346 +HRESULT WSLCSession::TagImage(const WSLCTagImageOptions* Options)
1347 +try
1348 +{
1349 + COMServiceExecutionContext context;
1350 +
1351 + RETURN_HR_IF_NULL(E_POINTER, Options);
1352 + RETURN_HR_IF_NULL(E_POINTER, Options->Image);
1353 + RETURN_HR_IF(E_INVALIDARG, strlen(Options->Image) > WSLC_MAX_IMAGE_NAME_LENGTH);
1354 + RETURN_HR_IF_NULL(E_POINTER, Options->Repo);
1355 + RETURN_HR_IF_NULL(E_POINTER, Options->Tag);
1356 + RETURN_HR_IF(E_INVALIDARG, strlen(Options->Repo) + strlen(Options->Tag) + 1 > WSLC_MAX_IMAGE_NAME_LENGTH);
1357 +
1358 + auto lock = m_lock.lock_shared();
1359 +
1360 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1361 +
1362 + try
1363 + {
1364 + m_dockerClient->TagImage(Options->Image, Options->Repo, Options->Tag);
1365 + }
1366 + catch (const DockerHTTPException& e)
1367 + {
1368 + std::string errorMessage;
1369 + if ((e.StatusCode() >= 400 && e.StatusCode() < 500))
1370 + {
1371 + errorMessage = e.DockerMessage<docker_schema::ErrorResponse>().message;
1372 + }
1373 +
1374 + THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), errorMessage, e.StatusCode() == 400);
1375 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_IMAGE_NOT_FOUND, errorMessage, e.StatusCode() == 404);
1376 + THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), errorMessage, e.StatusCode() == 409);
1377 + THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
1378 + }
1379 +
1380 + return S_OK;
1381 +}
1382 +CATCH_RETURN();
1383 +
1384 +HRESULT WSLCSession::PushImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback)
1385 +try
1386 +{
1387 + COMServiceExecutionContext context;
1388 +
1389 + RETURN_HR_IF_NULL(E_POINTER, Image);
1390 + RETURN_HR_IF_NULL(E_POINTER, RegistryAuthenticationInformation);
1391 +
1392 + auto lock = m_lock.lock_shared();
1393 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1394 +
1395 + auto [repo, tagOrDigest] = wslutil::ParseImage(Image);
1396 + auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, RegistryAuthenticationInformation);
1397 + StreamImageOperation(*requestContext, Image, "Push", ProgressCallback);
1398 +
1399 + return S_OK;
1400 +}
1401 +CATCH_RETURN();
1402 +
1403 +HRESULT WSLCSession::InspectImage(_In_ LPCSTR ImageNameOrId, _Out_ LPSTR* Output)
1404 +try
1405 +{
1406 + COMServiceExecutionContext context;
1407 +
1408 + RETURN_HR_IF_NULL(E_POINTER, ImageNameOrId);
1409 + RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrId) > WSLC_MAX_IMAGE_NAME_LENGTH);
1410 + RETURN_HR_IF_NULL(E_POINTER, Output);
1411 +
1412 + *Output = nullptr;
1413 +
1414 + auto lock = m_lock.lock_shared();
1415 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1416 +
1417 + docker_schema::InspectImage dockerInspect;
1418 + try
1419 + {
1420 + dockerInspect = m_dockerClient->InspectImage(ImageNameOrId);
1421 + }
1422 + catch (const DockerHTTPException& e)
1423 + {
1424 + std::string errorMessage = "Failed to inspect image";
1425 + if (e.HasErrorMessage())
1426 + {
1427 + errorMessage = e.DockerMessage<docker_schema::ErrorResponse>().message;
1428 + }
1429 +
1430 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_IMAGE_NOT_FOUND, errorMessage, e.StatusCode() == 404);
1431 + THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), errorMessage, e.StatusCode() == 400);
1432 + THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
1433 + }
1434 +
1435 + // Convert to WSLC schema
1436 + auto wslcInspect = ConvertInspectImage(dockerInspect);
1437 +
1438 + // Serialize to JSON
1439 + std::string wslcJson = wsl::shared::ToJson(wslcInspect);
1440 + *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(wslcJson.c_str()).release();
1441 +
1442 + return S_OK;
1443 +}
1444 +CATCH_RETURN();
1445 +
1446 +HRESULT WSLCSession::Authenticate(_In_ LPCSTR ServerAddress, _In_ LPCSTR Username, _In_ LPCSTR Password, _Out_ LPSTR* IdentityToken)
1447 +try
1448 +{
1449 + COMServiceExecutionContext context;
1450 +
1451 + RETURN_HR_IF_NULL(E_POINTER, ServerAddress);
1452 + RETURN_HR_IF_NULL(E_POINTER, Username);
1453 + RETURN_HR_IF_NULL(E_POINTER, Password);
1454 + RETURN_HR_IF_NULL(E_POINTER, IdentityToken);
1455 +
1456 + *IdentityToken = nullptr;
1457 +
1458 + auto lock = m_lock.lock_shared();
1459 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1460 +
1461 + wil::unique_cotaskmem_ansistring token;
1462 +
1463 + try
1464 + {
1465 + auto response = m_dockerClient->Authenticate(ServerAddress, Username, Password);
1466 + token = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(response.c_str());
1467 + }
1468 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to authenticate with registry: %hs", ServerAddress);
1469 +
1470 + *IdentityToken = token.release();
1471 + return S_OK;
1472 +}
1473 +CATCH_RETURN();
1474 +
1475 +HRESULT WSLCSession::PruneImages(const WSLCPruneImagesOptions* Options, WSLCDeletedImageInformation** DeletedImages, ULONG* DeletedImagesCount, ULONGLONG* SpaceReclaimed)
1476 +try
1477 +{
1478 + COMServiceExecutionContext context;
1479 +
1480 + RETURN_HR_IF_NULL(E_POINTER, DeletedImages);
1481 + RETURN_HR_IF_NULL(E_POINTER, DeletedImagesCount);
1482 + RETURN_HR_IF_NULL(E_POINTER, SpaceReclaimed);
1483 + *DeletedImages = nullptr;
1484 + *DeletedImagesCount = 0;
1485 + *SpaceReclaimed = 0;
1486 +
1487 + if (Options != nullptr)
1488 + {
1489 + RETURN_HR_IF(E_INVALIDARG, WI_IsFlagSet(Options->Flags, WSLCPruneImagesFlagsDanglingTrue) && WI_IsFlagSet(Options->Flags, WSLCPruneImagesFlagsDanglingFalse));
1490 + RETURN_HR_IF(E_INVALIDARG, WI_IsAnyFlagSet(static_cast<WSLCPruneImagesFlags>(Options->Flags), ~WSLCPruneImagesFlagsValid));
1491 + }
1492 +
1493 + auto lock = m_lock.lock_shared();
1494 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1495 +
1496 + DockerHTTPClient::PruneImagesFilters filters;
1497 +
1498 + if (Options != nullptr)
1499 + {
1500 + if (WI_IsFlagSet(Options->Flags, WSLCPruneImagesFlagsDanglingTrue))
1501 + {
1502 + filters.dangling = true;
1503 + }
1504 + else if (WI_IsFlagSet(Options->Flags, WSLCPruneImagesFlagsDanglingFalse))
1505 + {
1506 + filters.dangling = false;
1507 + }
1508 +
1509 + if (Options->Until > 0)
1510 + {
1511 + filters.until = Options->Until;
1512 + }
1513 +
1514 + if (Options->Labels != nullptr && Options->LabelsCount > 0)
1515 + {
1516 + for (ULONG i = 0; i < Options->LabelsCount; ++i)
1517 + {
1518 + const auto& filter = Options->Labels[i];
1519 + RETURN_HR_IF_NULL(E_POINTER, filter.Key);
1520 +
1521 + std::string labelFilter = filter.Key;
1522 + if (filter.Value != nullptr)
1523 + {
1524 + labelFilter += "=";
1525 + labelFilter += filter.Value;
1526 + }
1527 +
1528 + if (filter.Present)
1529 + {
1530 + filters.presentLabels.emplace_back(std::move(labelFilter));
1531 + }
1532 + else
1533 + {
1534 + filters.absentLabels.emplace_back(std::move(labelFilter));
1535 + }
1536 + }
1537 + }
1538 + }
1539 +
1540 + docker_schema::PruneImageResult pruneResult;
1541 + try
1542 + {
1543 + pruneResult = m_dockerClient->PruneImages(filters);
1544 + }
1545 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune images");
1546 +
1547 + *SpaceReclaimed = pruneResult.SpaceReclaimed;
1548 +
1549 + if (pruneResult.ImagesDeleted.has_value() && !pruneResult.ImagesDeleted->empty())
1550 + {
1551 + auto output = wil::make_unique_cotaskmem<WSLCDeletedImageInformation[]>(pruneResult.ImagesDeleted->size());
1552 + size_t index = 0;
1553 + for (const auto& image : pruneResult.ImagesDeleted.value())
1554 + {
1555 + THROW_HR_IF(
1556 + E_UNEXPECTED, (image.Deleted.empty() && image.Untagged.empty()) || (!image.Deleted.empty() && !image.Untagged.empty()));
1557 +
1558 + if (!image.Deleted.empty())
1559 + {
1560 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, image.Deleted.c_str()) != 0);
1561 + output[index].Type = WSLCDeletedImageTypeDeleted;
1562 + }
1563 + else
1564 + {
1565 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, image.Untagged.c_str()) != 0);
1566 + output[index].Type = WSLCDeletedImageTypeUntagged;
1567 + }
1568 +
1569 + index++;
1570 + }
1571 +
1572 + *DeletedImages = output.release();
1573 + *DeletedImagesCount = static_cast<ULONG>(pruneResult.ImagesDeleted->size());
1574 + }
1575 +
1576 + return S_OK;
1577 +}
1578 +CATCH_RETURN();
1579 +
1580 +HRESULT WSLCSession::CreateContainer(const WSLCContainerOptions* containerOptions, IWSLCContainer** Container)
1581 +try
1582 +{
1583 + COMServiceExecutionContext context;
1584 +
1585 + RETURN_HR_IF_NULL(E_POINTER, containerOptions);
1586 +
1587 + // Validate that Image is not null.
1588 + RETURN_HR_IF(E_INVALIDARG, containerOptions->Image == nullptr);
1589 +
1590 + auto lock = m_lock.lock_shared();
1591 +
1592 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
1593 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_eventTracker);
1594 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
1595 +
1596 + // Validate that name & images are valid.
1597 + if (containerOptions->Name != nullptr)
1598 + {
1599 + ValidateName(containerOptions->Name, WSLC_MAX_CONTAINER_NAME_LENGTH);
1600 + }
1601 +
1602 + RETURN_HR_IF(E_INVALIDARG, strlen(containerOptions->Image) > WSLC_MAX_IMAGE_NAME_LENGTH);
1603 +
1604 + // TODO: Log entrance into the function.
1605 +
1606 + try
1607 + {
1608 + std::scoped_lock lock(m_containersLock, m_volumesLock);
1609 +
1610 + auto& it = m_containers.emplace_back(WSLCContainerImpl::Create(
1611 + *containerOptions,
1612 + *this,
1613 + m_virtualMachine.value(),
1614 + m_volumes,
1615 + std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
1616 + m_eventTracker.value(),
1617 + m_dockerClient.value(),
1618 + m_ioRelay));
1619 +
1620 + it->CopyTo(Container);
1621 +
1622 + return S_OK;
1623 + }
1624 + catch (const DockerHTTPException& e)
1625 + {
1626 + std::string errorMessage;
1627 + if ((e.StatusCode() >= 400 && e.StatusCode() < 500))
1628 + {
1629 + errorMessage = e.DockerMessage<docker_schema::ErrorResponse>().message;
1630 + }
1631 +
1632 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_IMAGE_NOT_FOUND, errorMessage, e.StatusCode() == 404);
1633 + THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), errorMessage, e.StatusCode() == 409);
1634 + THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
1635 + }
1636 +}
1637 +CATCH_RETURN();
1638 +
1639 +HRESULT WSLCSession::OpenContainer(LPCSTR Id, IWSLCContainer** Container)
1640 +try
1641 +{
1642 + COMServiceExecutionContext context;
1643 +
1644 + ValidateName(Id, WSLC_MAX_CONTAINER_NAME_LENGTH);
1645 +
1646 + // Look for an exact ID match first.
1647 + auto lock = m_lock.lock_shared();
1648 + std::lock_guard containersLock{m_containersLock};
1649 +
1650 + // Purge containers that were auto-deleted via OnEvent (--rm).
1651 + std::erase_if(m_containers, [](const auto& e) { return e->State() == WslcContainerStateDeleted; });
1652 + auto it = std::ranges::find_if(m_containers, [Id](const auto& e) { return e->ID() == Id; });
1653 +
1654 + // If no match is found, call Inspect() so that partial IDs and names are matched.
1655 + if (it == m_containers.end())
1656 + {
1657 + // TODO: consider a trimmed down version of inspect to avoid parsing the full response.
1658 + docker_schema::InspectContainer inspectResult;
1659 +
1660 + try
1661 + {
1662 + inspectResult = m_dockerClient->InspectContainer(Id);
1663 + }
1664 + catch (DockerHTTPException& e)
1665 + {
1666 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerNotFound(Id), e.StatusCode() == 404);
1667 + RETURN_HR_IF_MSG(WSLC_E_CONTAINER_PREFIX_AMBIGUOUS, e.StatusCode() == 400, "Ambiguous prefix: '%hs'", Id);
1668 +
1669 + THROW_HR_MSG(E_FAIL, "Unexpected error inspecting container '%hs': %hs", Id, e.what());
1670 + }
1671 +
1672 + it = std::ranges::find_if(m_containers, [&](const auto& e) { return e->ID() == inspectResult.Id; });
1673 + RETURN_HR_IF_MSG(
1674 + E_UNEXPECTED, it == m_containers.end(), "Resolved container ID (%hs -> %hs) not found", Id, inspectResult.Id.c_str());
1675 + }
1676 +
1677 + auto result = wil::ResultFromException([&]() { (*it)->CopyTo(Container); });
1678 +
1679 + // Return WSLC_E_CONTAINER_NOT_FOUND if the container was found, but is being deleted for consistency.
1680 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerNotFound(Id), result == RPC_E_DISCONNECTED);
1681 +
1682 + return result;
1683 +}
1684 +CATCH_RETURN();
1685 +
1686 +HRESULT WSLCSession::ListContainers(WSLCContainerEntry** Containers, ULONG* Count, WSLCContainerPortMapping** Ports, ULONG* PortsCount)
1687 +try
1688 +{
1689 + COMServiceExecutionContext context;
1690 +
1691 + *Count = 0;
1692 + *Containers = nullptr;
1693 + *Ports = nullptr;
1694 + *PortsCount = 0;
1695 +
1696 + auto lock = m_lock.lock_shared();
1697 + std::lock_guard containersLock{m_containersLock};
1698 +
1699 + // Purge containers that were auto-deleted via OnEvent (--rm).
1700 + std::erase_if(m_containers, [](const auto& e) { return e->State() == WslcContainerStateDeleted; });
1701 +
1702 + auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(m_containers.size());
1703 + std::vector<WSLCContainerPortMapping> allPorts;
1704 +
1705 + size_t index = 0;
1706 + for (const auto& e : m_containers)
1707 + {
1708 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, e->Image().c_str()) != 0);
1709 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Name, e->Name().c_str()) != 0);
1710 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Id, e->ID().c_str()) != 0);
1711 + e->GetState(&output[index].State);
1712 + e->GetStateChangedAt(&output[index].StateChangedAt);
1713 + e->GetCreatedAt(&output[index].CreatedAt);
1714 +
1715 + for (const auto& port : e->GetPorts())
1716 + {
1717 + WSLCContainerPortMapping mapping{};
1718 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(mapping.Id, e->ID().c_str()) != 0);
1719 + mapping.PortMapping.HostPort = port.HostPort;
1720 + mapping.PortMapping.ContainerPort = port.ContainerPort;
1721 + mapping.PortMapping.Family = port.Family;
1722 + mapping.PortMapping.Protocol = port.Protocol;
1723 + THROW_HR_IF(E_UNEXPECTED, port.BindingAddress.size() > WSLC_MAX_BINDING_ADDRESS_LENGTH);
1724 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(mapping.PortMapping.BindingAddress, port.BindingAddress.c_str()) != 0);
1725 + allPorts.push_back(mapping);
1726 + }
1727 +
1728 + index++;
1729 + }
1730 +
1731 + *Count = static_cast<ULONG>(m_containers.size());
1732 + *Containers = output.release();
1733 +
1734 + if (!allPorts.empty())
1735 + {
1736 + auto portsOutput = wil::make_unique_cotaskmem<WSLCContainerPortMapping[]>(allPorts.size());
1737 + memcpy(portsOutput.get(), allPorts.data(), allPorts.size() * sizeof(WSLCContainerPortMapping));
1738 + *PortsCount = static_cast<ULONG>(allPorts.size());
1739 + *Ports = portsOutput.release();
1740 + }
1741 +
1742 + return S_OK;
1743 +}
1744 +CATCH_RETURN();
1745 +
1746 +HRESULT WSLCSession::PruneContainers(_In_opt_ WSLCPruneLabelFilter* Filters, _In_ DWORD FiltersCount, _In_ ULONGLONG Until, _Out_ WSLCPruneContainersResults* Result)
1747 +try
1748 +{
1749 + COMServiceExecutionContext context;
1750 +
1751 + RETURN_HR_IF_NULL(E_POINTER, Result);
1752 + ZeroMemory(Result, sizeof(*Result));
1753 +
1754 + DockerHTTPClient::PruneContainersFilters filters;
1755 +
1756 + if (FiltersCount > 0)
1757 + {
1758 + THROW_HR_IF(E_POINTER, FiltersCount > 0 && Filters == nullptr);
1759 +
1760 + for (DWORD i = 0; i < FiltersCount; ++i)
1761 + {
1762 + THROW_HR_IF_MSG(E_POINTER, Filters[i].Key == nullptr, "Filter key cannot be null (index %lu)", i);
1763 + std::string labelFilter = Filters[i].Key;
1764 +
1765 + if (Filters[i].Value != nullptr)
1766 + {
1767 + labelFilter += '=';
1768 + labelFilter += Filters[i].Value;
1769 + }
1770 +
1771 + if (Filters[i].Present)
1772 + {
1773 + filters.presentLabels.emplace_back(std::move(labelFilter));
1774 + }
1775 + else
1776 + {
1777 + filters.absentLabels.emplace_back(std::move(labelFilter));
1778 + }
1779 + }
1780 + }
1781 +
1782 + if (Until > 0)
1783 + {
1784 + filters.until = Until;
1785 + }
1786 +
1787 + auto lock = m_lock.lock_shared();
1788 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1789 +
1790 + std::lock_guard containersLock{m_containersLock};
1791 +
1792 + docker_schema::PruneContainerResult pruneResult;
1793 +
1794 + try
1795 + {
1796 + pruneResult = m_dockerClient->PruneContainers(filters);
1797 + }
1798 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune containers");
1799 +
1800 + Result->SpaceReclaimed = pruneResult.SpaceReclaimed;
1801 +
1802 + if (pruneResult.ContainersDeleted.has_value() && pruneResult.ContainersDeleted->size() > 0)
1803 + {
1804 + // Remove deleted containers from m_containers.
1805 + auto pred = [&](const auto& e) {
1806 + return std::ranges::find(pruneResult.ContainersDeleted.value(), e->ID()) != pruneResult.ContainersDeleted->end();
1807 + };
1808 +
1809 + auto erased = std::erase_if(m_containers, pred);
1810 + LOG_HR_IF_MSG(
1811 + E_UNEXPECTED,
1812 + erased != pruneResult.ContainersDeleted->size(),
1813 + "Expected to erase %zu containers, but erased %zu",
1814 + pruneResult.ContainersDeleted->size(),
1815 + erased);
1816 +
1817 + auto containers = wil::make_unique_cotaskmem<WSLCContainerId[]>(pruneResult.ContainersDeleted->size());
1818 +
1819 + for (size_t i = 0; i < pruneResult.ContainersDeleted->size(); ++i)
1820 + {
1821 + THROW_HR_IF_MSG(
1822 + E_UNEXPECTED,
1823 + strcpy_s(containers[i], pruneResult.ContainersDeleted.value()[i].c_str()) != 0,
1824 + "Unexpected container name: %hs",
1825 + pruneResult.ContainersDeleted.value()[i].c_str());
1826 + }
1827 +
1828 + Result->Containers = containers.release();
1829 + Result->ContainersCount = static_cast<DWORD>(pruneResult.ContainersDeleted->size());
1830 + }
1831 + else
1832 + {
1833 + Result->Containers = nullptr;
1834 + Result->ContainersCount = 0;
1835 + }
1836 +
1837 + return S_OK;
1838 +}
1839 +CATCH_RETURN();
1840 +
1841 +HRESULT WSLCSession::CreateRootNamespaceProcess(LPCSTR Executable, const WSLCProcessOptions* Options, IWSLCProcess** Process, int* Errno)
1842 +try
1843 +{
1844 + COMServiceExecutionContext context;
1845 +
1846 + if (Errno != nullptr)
1847 + {
1848 + *Errno = -1; // Make sure not to return 0 if something fails.
1849 + }
1850 +
1851 + auto lock = m_lock.lock_shared();
1852 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
1853 +
1854 + auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, Errno);
1855 + THROW_IF_FAILED(process.CopyTo(Process));
1856 +
1857 + return S_OK;
1858 +}
1859 +CATCH_RETURN();
1860 +
1861 +void WSLCSession::Ext4Format(const std::string& Device)
1862 +{
1863 + constexpr auto mkfsPath = "/usr/sbin/mkfs.ext4";
1864 + ServiceProcessLauncher launcher(mkfsPath, {mkfsPath, Device});
1865 + auto result = launcher.Launch(*m_virtualMachine).WaitAndCaptureOutput();
1866 +
1867 + THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
1868 +}
1869 +
1870 +HRESULT WSLCSession::FormatVirtualDisk(LPCWSTR Path)
1871 +try
1872 +{
1873 + COMServiceExecutionContext context;
1874 +
1875 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Path), !std::filesystem::path(Path).is_absolute());
1876 +
1877 + auto lock = m_lock.lock_shared();
1878 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
1879 +
1880 + // Attach the disk to the VM (AttachDisk() performs the access check for the VHD file).
1881 + auto [lun, device] = m_virtualMachine->AttachDisk(Path, false);
1882 +
1883 + // N.B. DetachDisk calls sync() before detaching.
1884 + auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_virtualMachine->DetachDisk(lun); });
1885 +
1886 + // Format it to ext4.
1887 + m_virtualMachine->Ext4Format(device);
1888 +
1889 + return S_OK;
1890 +}
1891 +CATCH_RETURN();
1892 +
1893 +HRESULT WSLCSession::CreateVolume(const WSLCVolumeOptions* Options, WSLCVolumeInformation* VolumeInfo)
1894 +try
1895 +{
1896 + COMServiceExecutionContext context;
1897 +
1898 + RETURN_HR_IF_NULL(E_POINTER, Options);
1899 + RETURN_HR_IF_NULL(E_POINTER, VolumeInfo);
1900 + ZeroMemory(VolumeInfo, sizeof(*VolumeInfo));
1901 +
1902 + // Default driver to "guest" if not specified.
1903 + std::string driver = (Options->Driver != nullptr && *Options->Driver != '\0') ? Options->Driver : WSLCGuestVolumeDriver;
1904 +
1905 + THROW_HR_WITH_USER_ERROR_IF(
1906 + E_INVALIDARG, Localization::MessageWslcInvalidVolumeType(driver), driver != WSLCVhdVolumeDriver && driver != WSLCGuestVolumeDriver);
1907 +
1908 + auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount);
1909 + auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel);
1910 +
1911 + auto lock = m_lock.lock_shared();
1912 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
1913 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
1914 +
1915 + std::lock_guard volumesLock(m_volumesLock);
1916 +
1917 + if (Options->Name != nullptr && Options->Name[0] != '\0')
1918 + {
1919 + ValidateName(Options->Name, WSLC_MAX_VOLUME_NAME_LENGTH);
1920 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_volumes.contains(Options->Name));
1921 + }
1922 +
1923 + std::unique_ptr<IWSLCVolume> volume;
1924 + if (driver == WSLCVhdVolumeDriver)
1925 + {
1926 + WSL_LOG("VolumeCreatedVhdDriver", TraceLoggingValue(Options->Name ? Options->Name : "", "VolumeName"));
1927 + volume = WSLCVhdVolumeImpl::Create(
1928 + Options->Name,
1929 + std::move(driverOpts),
1930 + std::move(labels),
1931 + m_storageVhdPath.parent_path(),
1932 + m_virtualMachine.value(),
1933 + m_dockerClient.value());
1934 + }
1935 + else
1936 + {
1937 + WI_ASSERT(driver == WSLCGuestVolumeDriver);
1938 + volume = WSLCGuestVolumeImpl::Create(Options->Name, std::move(driverOpts), std::move(labels), m_dockerClient.value());
1939 + }
1940 +
1941 + const auto& name = volume->Name();
1942 + auto info = volume->GetVolumeInformation();
1943 +
1944 + auto [it, inserted] = m_volumes.insert({name, std::move(volume)});
1945 + WI_VERIFY(inserted);
1946 +
1947 + WSL_LOG("VolumeCreated", TraceLoggingValue(name.c_str(), "VolumeName"));
1948 +
1949 + *VolumeInfo = info;
1950 + return S_OK;
1951 +}
1952 +CATCH_RETURN();
1953 +
1954 +HRESULT WSLCSession::DeleteVolume(LPCSTR Name)
1955 +try
1956 +{
1957 + COMServiceExecutionContext context;
1958 +
1959 + RETURN_HR_IF_NULL(E_POINTER, Name);
1960 + std::string name = Name;
1961 + ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH);
1962 +
1963 + auto lock = m_lock.lock_shared();
1964 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
1965 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
1966 +
1967 + std::lock_guard volumesLock(m_volumesLock);
1968 +
1969 + auto it = m_volumes.find(name);
1970 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(name), it == m_volumes.end());
1971 +
1972 + it->second->Delete();
1973 + m_volumes.erase(it);
1974 + WSL_LOG("VolumeDeleted", TraceLoggingValue(name.c_str(), "VolumeName"));
1975 +
1976 + return S_OK;
1977 +}
1978 +CATCH_RETURN();
1979 +
1980 +HRESULT WSLCSession::ListVolumes(WSLCVolumeInformation** Volumes, ULONG* Count)
1981 +try
1982 +{
1983 + COMServiceExecutionContext context;
1984 +
1985 + RETURN_HR_IF_NULL(E_POINTER, Volumes);
1986 + RETURN_HR_IF_NULL(E_POINTER, Count);
1987 +
1988 + *Volumes = nullptr;
1989 + *Count = 0;
1990 +
1991 + auto lock = m_lock.lock_shared();
1992 + std::lock_guard volumesLock(m_volumesLock);
1993 +
1994 + if (m_volumes.empty())
1995 + {
1996 + return S_OK;
1997 + }
1998 +
1999 + auto output = wil::make_unique_cotaskmem<WSLCVolumeInformation[]>(m_volumes.size());
2000 +
2001 + ULONG index = 0;
2002 + for (const auto& [name, vol] : m_volumes)
2003 + {
2004 + output[index] = vol->GetVolumeInformation();
2005 + index++;
2006 + }
2007 +
2008 + *Volumes = output.release();
2009 + *Count = index;
2010 +
2011 + return S_OK;
2012 +}
2013 +CATCH_RETURN();
2014 +
2015 +HRESULT WSLCSession::InspectVolume(LPCSTR Name, LPSTR* Output)
2016 +try
2017 +{
2018 + COMServiceExecutionContext context;
2019 +
2020 + RETURN_HR_IF_NULL(E_POINTER, Name);
2021 + RETURN_HR_IF_NULL(E_POINTER, Output);
2022 +
2023 + *Output = nullptr;
2024 +
2025 + std::string name = Name;
2026 + ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH);
2027 +
2028 + auto lock = m_lock.lock_shared();
2029 + std::lock_guard volumesLock(m_volumesLock);
2030 +
2031 + auto it = m_volumes.find(name);
2032 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(name), it == m_volumes.end());
2033 +
2034 + const auto& volume = it->second;
2035 +
2036 + std::string json = volume->Inspect();
2037 + *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
2038 +
2039 + return S_OK;
2040 +}
2041 +CATCH_RETURN();
2042 +
2043 +HRESULT WSLCSession::PruneVolumes(const WSLCPruneVolumesOptions* /*Options*/, WSLCPruneVolumesResults* /*Results*/)
2044 +{
2045 + // TODO: Implement volume pruning. Docker's volume prune API skips bind-mount volumes,
2046 + // so WSLC VHD volumes require custom handling.
2047 + return E_NOTIMPL;
2048 +}
2049 +
2050 +int WSLCSession::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs)
2051 +{
2052 + LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGTERM));
2053 +
2054 + try
2055 + {
2056 + return Process.Wait(TerminateTimeoutMs);
2057 + }
2058 + catch (...)
2059 + {
2060 + LOG_CAUGHT_EXCEPTION();
2061 + try
2062 + {
2063 + LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGKILL));
2064 + return Process.Wait(KillTimeoutMs);
2065 + }
2066 + CATCH_LOG();
2067 + }
2068 +
2069 + return -1;
2070 +}
2071 +// Network management.
2072 +
2073 +HRESULT WSLCSession::CreateNetwork(const WSLCNetworkOptions* Options)
2074 +try
2075 +{
2076 + COMServiceExecutionContext context;
2077 +
2078 + RETURN_HR_IF_NULL(E_POINTER, Options);
2079 + RETURN_HR_IF_NULL(E_POINTER, Options->Name);
2080 + RETURN_HR_IF_NULL(E_POINTER, Options->Driver);
2081 +
2082 + std::string name = Options->Name;
2083 + std::string driver = Options->Driver;
2084 +
2085 + ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);
2086 +
2087 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidName(name), IsReservedNetworkName(name));
2088 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidNetworkDriver(driver), driver != WSLCBridgeNetworkDriver);
2089 +
2090 + auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount);
2091 + auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel);
2092 +
2093 + auto lock = m_lock.lock_shared();
2094 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
2095 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2096 +
2097 + std::lock_guard networksLock(m_networksLock);
2098 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_networks.contains(name));
2099 +
2100 + docker_schema::CreateNetwork request;
2101 + request.Name = name;
2102 + request.Driver = driver;
2103 + request.Labels = labels;
2104 + request.Labels[WSLCNetworkManagedLabel] = "true";
2105 +
2106 + if (auto it = driverOpts.find("Internal"); it != driverOpts.end())
2107 + {
2108 + request.Internal = (it->second == "true");
2109 + }
2110 +
2111 + if (auto it = driverOpts.find("Subnet"); it != driverOpts.end())
2112 + {
2113 + docker_schema::IPAMConfig ipamConfig;
2114 + ipamConfig.Subnet = it->second;
2115 +
2116 + auto gatewayIt = driverOpts.find("Gateway");
2117 + if (gatewayIt != driverOpts.end())
2118 + {
2119 + ipamConfig.Gateway = gatewayIt->second;
2120 + }
2121 +
2122 + auto& ipam = request.IPAM.emplace();
2123 + ipam.Driver = "default";
2124 + ipam.Config.emplace().push_back(std::move(ipamConfig));
2125 + }
2126 +
2127 + try
2128 + {
2129 + m_dockerClient->CreateNetwork(request);
2130 + }
2131 + catch (const DockerHTTPException& e)
2132 + {
2133 + THROW_HR_WITH_USER_ERROR_IF(
2134 + HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), Localization::MessageWslcNetworkAlreadyExists(name), e.StatusCode() == 409);
2135 + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to create network '%hs'", name.c_str());
2136 + }
2137 +
2138 + auto removeNetworkCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_dockerClient->RemoveNetwork(name); });
2139 +
2140 + // Inspect the newly created network to cache full properties (IPAM, Scope, etc.)
2141 + // since CreateNetworkResponse only returns {Id, Warning}.
2142 + docker_schema::Network full;
2143 + try
2144 + {
2145 + full = m_dockerClient->InspectNetwork(name);
2146 + }
2147 + catch (const DockerHTTPException& e)
2148 + {
2149 + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to inspect newly created network '%hs'", name.c_str());
2150 + }
2151 +
2152 + NetworkEntry entry;
2153 + entry.Id = full.Id;
2154 + entry.Driver = full.Driver;
2155 + entry.Scope = full.Scope;
2156 + entry.Internal = full.Internal;
2157 + entry.Labels = full.Labels;
2158 + entry.IPAM.Driver = full.IPAM.Driver;
2159 + if (full.IPAM.Config)
2160 + {
2161 + auto& cfgs = entry.IPAM.Config.emplace();
2162 + for (const auto& c : *full.IPAM.Config)
2163 + {
2164 + cfgs.push_back({c.Subnet, c.Gateway});
2165 + }
2166 + }
2167 +
2168 + auto [it, inserted] = m_networks.insert({name, std::move(entry)});
2169 + WI_VERIFY(inserted);
2170 +
2171 + WSL_LOG("NetworkCreated", TraceLoggingValue(name.c_str(), "NetworkName"), TraceLoggingValue(full.Id.c_str(), "NetworkId"));
2172 +
2173 + removeNetworkCleanup.release();
2174 +
2175 + return S_OK;
2176 +}
2177 +CATCH_RETURN();
2178 +
2179 +HRESULT WSLCSession::DeleteNetwork(LPCSTR Name)
2180 +try
2181 +{
2182 + COMServiceExecutionContext context;
2183 +
2184 + RETURN_HR_IF_NULL(E_POINTER, Name);
2185 + std::string name = Name;
2186 + ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);
2187 +
2188 + auto lock = m_lock.lock_shared();
2189 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
2190 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2191 +
2192 + std::lock_guard networksLock(m_networksLock);
2193 +
2194 + auto it = m_networks.find(name);
2195 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), it == m_networks.end());
2196 +
2197 + try
2198 + {
2199 + m_dockerClient->RemoveNetwork(name);
2200 + }
2201 + catch (const DockerHTTPException& e)
2202 + {
2203 + THROW_HR_WITH_USER_ERROR_IF(
2204 + HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), Localization::MessageWslcNetworkInUse(name), e.StatusCode() == 409);
2205 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), e.StatusCode() == 404);
2206 + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to delete network '%hs'", name.c_str());
2207 + }
2208 +
2209 + m_networks.erase(it);
2210 + WSL_LOG("NetworkDeleted", TraceLoggingValue(name.c_str(), "NetworkName"));
2211 +
2212 + return S_OK;
2213 +}
2214 +CATCH_RETURN();
2215 +
2216 +HRESULT WSLCSession::ListNetworks(WSLCNetworkInformation** Networks, ULONG* Count)
2217 +try
2218 +{
2219 + COMServiceExecutionContext context;
2220 +
2221 + RETURN_HR_IF_NULL(E_POINTER, Networks);
2222 + RETURN_HR_IF_NULL(E_POINTER, Count);
2223 +
2224 + *Networks = nullptr;
2225 + *Count = 0;
2226 +
2227 + auto lock = m_lock.lock_shared();
2228 + std::lock_guard networksLock(m_networksLock);
2229 +
2230 + if (m_networks.empty())
2231 + {
2232 + return S_OK;
2233 + }
2234 +
2235 + auto output = wil::make_unique_cotaskmem<WSLCNetworkInformation[]>(m_networks.size());
2236 +
2237 + ULONG index = 0;
2238 + for (const auto& [name, entry] : m_networks)
2239 + {
2240 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Name, name.c_str()) != 0);
2241 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Id, entry.Id.c_str()) != 0);
2242 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Driver, entry.Driver.c_str()) != 0);
2243 + index++;
2244 + }
2245 +
2246 + *Networks = output.release();
2247 + *Count = index;
2248 +
2249 + return S_OK;
2250 +}
2251 +CATCH_RETURN();
2252 +
2253 +HRESULT WSLCSession::InspectNetwork(LPCSTR Name, LPSTR* Output)
2254 +try
2255 +{
2256 + COMServiceExecutionContext context;
2257 +
2258 + RETURN_HR_IF_NULL(E_POINTER, Name);
2259 + RETURN_HR_IF_NULL(E_POINTER, Output);
2260 +
2261 + *Output = nullptr;
2262 +
2263 + std::string name = Name;
2264 + ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);
2265 +
2266 + auto lock = m_lock.lock_shared();
2267 + std::lock_guard networksLock(m_networksLock);
2268 +
2269 + auto it = m_networks.find(name);
2270 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), it == m_networks.end());
2271 +
2272 + const auto& entry = it->second;
2273 +
2274 + wslc_schema::InspectNetwork result;
2275 + result.Id = entry.Id;
2276 + result.Name = name;
2277 + result.Driver = entry.Driver;
2278 + result.Scope = entry.Scope;
2279 + result.Internal = entry.Internal;
2280 + result.Labels = entry.Labels;
2281 +
2282 + result.IPAM.Driver = entry.IPAM.Driver;
2283 + if (entry.IPAM.Config)
2284 + {
2285 + auto& configs = result.IPAM.Config.emplace();
2286 + for (const auto& cfg : *entry.IPAM.Config)
2287 + {
2288 + wslc_schema::InspectIPAMConfig inspectCfg;
2289 + inspectCfg.Subnet = cfg.Subnet;
2290 + inspectCfg.Gateway = cfg.Gateway;
2291 + configs.push_back(std::move(inspectCfg));
2292 + }
2293 + }
2294 +
2295 + std::string json = wsl::shared::ToJson(result);
2296 + *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
2297 +
2298 + return S_OK;
2299 +}
2300 +CATCH_RETURN();
2301 +
2302 +HRESULT WSLCSession::Terminate()
2303 +try
2304 +{
2305 + // Ensure only one Terminate() runs. This must be checked before taking m_lock
2306 + // because OnVmExited() is called from the IORelay thread — if an external Terminate()
2307 + // holds m_lock and calls m_ioRelay.Stop(), the relay thread must not re-enter
2308 + // Terminate() and deadlock on m_lock.
2309 + if (m_terminating.exchange(true))
2310 + {
2311 + return S_OK;
2312 + }
2313 +
2314 + wil::rwlock_release_exclusive_scope_exit sessionLock;
2315 +
2316 + // Because it's not possible to synchronize CancelIoEx() with ReadFile() calls, keep attempting to acquire the session lock while cancelling IO & callbacks.
2317 + // This is required because calling CancelIoEx() between two ReadFile() calls does nothing, and therefore could still allow another thread to get stuck doing synchronous IO.
2318 + bool retrying = false;
2319 + while (!sessionLock)
2320 + {
2321 + // If this isn't the first iteration, sleep to prevent this loop from burning too much CPU.
2322 + if (retrying)
2323 + {
2324 + std::this_thread::sleep_for(std::chrono::milliseconds(10));
2325 + }
2326 +
2327 + {
2328 + std::lock_guard lock(m_userHandlesLock);
2329 +
2330 + // m_sessionTerminatingEvent is always valid, so it can be signalled without holding m_lock.
2331 + // This allows a session to be unblocked if a stuck operation is holding m_lock.
2332 + // N.B. This must happen under m_userHandlesLock to synchronize with potentially running operations.
2333 + if (!m_sessionTerminatingEvent.is_signaled())
2334 + {
2335 + m_sessionTerminatingEvent.SetEvent();
2336 + }
2337 +
2338 + // Cancel any pending IO on user-provided handles to unblock operations
2339 + // in case the handles don't support overlapped IO.
2340 + CancelUserHandleIO();
2341 + }
2342 +
2343 + {
2344 + std::lock_guard comLock(m_userCOMCallbacksLock);
2345 +
2346 + // Cancel any pending outgoing COM callback calls (e.g. IProgressCallback::OnProgress)
2347 + // to unblock operations waiting for cross-process COM responses.
2348 + CancelUserCOMCallbacks();
2349 + }
2350 +
2351 + sessionLock = m_lock.try_lock_exclusive();
2352 + retrying = true;
2353 + }
2354 +
2355 + // Acquire an exclusive lock to ensure that no operation is running.
2356 + WI_VERIFY(sessionLock);
2357 +
2358 + std::lock_guard containersLock(m_containersLock);
2359 + std::lock_guard volumesLock(m_volumesLock);
2360 + std::lock_guard networksLock(m_networksLock);
2361 +
2362 + m_containers.clear();
2363 + m_volumes.clear();
2364 + m_networks.clear();
2365 +
2366 + // Stop the IO relay.
2367 + // This stops:
2368 + // - container state monitoring.
2369 + // - container init process relays
2370 + // - execs relays
2371 + // - container logs relays
2372 + m_ioRelay.Stop();
2373 +
2374 + {
2375 + std::lock_guard allocatedPortsLock(m_allocatedPortsLock);
2376 + m_allocatedPorts.clear();
2377 + }
2378 +
2379 + m_eventTracker.reset();
2380 + m_dockerClient.reset();
2381 +
2382 + // Check if the VM has already exited (e.g., killed externally).
2383 + // If so, skip operations that require a live VM to avoid unnecessary waits.
2384 + // N.B. m_vmExitedEvent may be uninitialized if Terminate() is called from the
2385 + // Initialize() error path before GetTerminationEvent() succeeds.
2386 + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled())
2387 + {
2388 + WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId"));
2389 + }
2390 + else
2391 + {
2392 + // Stop dockerd first, then containerd (dockerd is a client of containerd).
2393 + // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened.
2394 + if (m_dockerdProcess.has_value())
2395 + {
2396 + auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs);
2397 + WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code"));
2398 + }
2399 +
2400 + if (m_containerdProcess.has_value())
2401 + {
2402 + auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs);
2403 + WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code"));
2404 + }
2405 +
2406 + if (m_virtualMachine)
2407 + {
2408 + // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running.
2409 + try
2410 + {
2411 + m_virtualMachine->Unmount(c_containerdStorage);
2412 + }
2413 + CATCH_LOG();
2414 + }
2415 + }
2416 +
2417 + m_dockerdProcess.reset();
2418 + m_containerdProcess.reset();
2419 + m_virtualMachine.reset();
2420 +
2421 + m_terminated = true;
2422 + return S_OK;
2423 +}
2424 +CATCH_RETURN();
2425 +
2426 +HRESULT WSLCSession::MountWindowsFolder(LPCWSTR WindowsPath, LPCSTR LinuxPath, BOOL ReadOnly)
2427 +try
2428 +{
2429 + COMServiceExecutionContext context;
2430 +
2431 + auto lock = m_lock.lock_shared();
2432 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2433 +
2434 + return m_virtualMachine->MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly);
2435 +}
2436 +CATCH_RETURN();
2437 +
2438 +HRESULT WSLCSession::UnmountWindowsFolder(LPCSTR LinuxPath)
2439 +try
2440 +{
2441 + COMServiceExecutionContext context;
2442 +
2443 + auto lock = m_lock.lock_shared();
2444 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2445 +
2446 + return m_virtualMachine->UnmountWindowsFolder(LinuxPath);
2447 +}
2448 +CATCH_RETURN();
2449 +
2450 +HRESULT WSLCSession::MapVmPort(int Family, unsigned short WindowsPort, unsigned short LinuxPort)
2451 +try
2452 +{
2453 + COMServiceExecutionContext context;
2454 +
2455 + auto lock = m_lock.lock_shared();
2456 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2457 +
2458 + std::lock_guard allocatedPortsLock(m_allocatedPortsLock);
2459 +
2460 + // Look for an existing allocation first.
2461 + auto it = m_allocatedPorts.find(LinuxPort);
2462 +
2463 + bool inserted = false;
2464 + auto cleanup = wil::scope_exit([&]() {
2465 + if (inserted)
2466 + {
2467 + m_allocatedPorts.erase(it);
2468 + }
2469 + });
2470 +
2471 + if (it == m_allocatedPorts.end())
2472 + {
2473 + // No existing port allocation, create a new one.
2474 + auto allocated = std::make_pair(m_virtualMachine->TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast<size_t>(0));
2475 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), allocated.first == nullptr);
2476 +
2477 + it = m_allocatedPorts.emplace(LinuxPort, allocated).first;
2478 + inserted = true;
2479 + }
2480 +
2481 + auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort);
2482 + mapping.AssignVmPort(it->second.first);
2483 +
2484 + m_virtualMachine->MapPort(mapping);
2485 +
2486 + // Increase usage count.
2487 + it->second.second++;
2488 +
2489 + mapping.Release();
2490 + cleanup.release();
2491 +
2492 + return S_OK;
2493 +}
2494 +CATCH_RETURN();
2495 +
2496 +HRESULT WSLCSession::UnmapVmPort(int Family, unsigned short WindowsPort, unsigned short LinuxPort)
2497 +try
2498 +{
2499 + COMServiceExecutionContext context;
2500 +
2501 + auto lock = m_lock.lock_shared();
2502 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2503 +
2504 + std::lock_guard allocatedPortsLock(m_allocatedPortsLock);
2505 +
2506 + auto it = m_allocatedPorts.find(LinuxPort);
2507 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_allocatedPorts.end());
2508 +
2509 + auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort);
2510 + mapping.AssignVmPort(it->second.first);
2511 + mapping.Attach(m_virtualMachine.value());
2512 +
2513 + auto cleanup = wil::scope_exit([&]() { mapping.Release(); });
2514 +
2515 + m_virtualMachine->UnmapPort(mapping);
2516 +
2517 + it->second.second--;
2518 +
2519 + // If usage count drops to 0, release the port allocation.
2520 + if (it->second.second == 0)
2521 + {
2522 + m_allocatedPorts.erase(it);
2523 + }
2524 +
2525 + return S_OK;
2526 +}
2527 +CATCH_RETURN();
2528 +
2529 +HRESULT WSLCSession::InterfaceSupportsErrorInfo(REFIID riid)
2530 +{
2531 + return riid == __uuidof(IWSLCSession) ? S_OK : S_FALSE;
2532 +}
2533 +
2534 +MultiHandleWait WSLCSession::CreateIOContext(HANDLE CancelHandle)
2535 +{
2536 + relay::MultiHandleWait io;
2537 +
2538 + // Cancel with E_ABORT if the session is terminating.
2539 + io.AddHandle(std::make_unique<relay::EventHandle>(
2540 + m_sessionTerminatingEvent.get(), [this]() { THROW_HR_MSG(E_ABORT, "Session %lu is terminating", m_id); }));
2541 +
2542 + // Cancel with E_ABORT if the client process exits.
2543 + io.AddHandle(std::make_unique<relay::EventHandle>(
2544 + wslutil::OpenCallingProcess(SYNCHRONIZE), [this]() { THROW_HR_MSG(E_ABORT, "Client process has exited"); }));
2545 +
2546 + if (CancelHandle != nullptr)
2547 + {
2548 + io.AddHandle(
2549 + std::make_unique<relay::EventHandle>(CancelHandle, []() { THROW_HR_MSG(E_ABORT, "Cancellation handle was signaled"); }));
2550 + }
2551 +
2552 + return io;
2553 +}
2554 +
2555 +UserHandle WSLCSession::OpenUserHandle(WSLCHandle Handle)
2556 +{
2557 + std::lock_guard lock(m_userHandlesLock);
2558 +
2559 + // Don't allow new handles to be added to the list if the session is terminating.
2560 + // N.B. This check must happen under m_userHandlesLock to synchronize with Terminate().
2561 +
2562 + THROW_HR_IF_MSG(
2563 + E_ABORT, m_sessionTerminatingEvent.is_signaled(), "Refusing to open a user handle while the session is terminating.");
2564 +
2565 + auto userHandle = common::wslutil::FromCOMInputHandle(Handle);
2566 +
2567 + m_userHandles.emplace_back(userHandle);
2568 +
2569 + return UserHandle{*this, userHandle};
2570 +}
2571 +
2572 +void WSLCSession::ReleaseUserHandle(HANDLE Handle)
2573 +{
2574 + std::lock_guard lock(m_userHandlesLock);
2575 +
2576 + auto it = std::ranges::find(m_userHandles, Handle);
2577 + WI_ASSERT(it != m_userHandles.end());
2578 +
2579 + m_userHandles.erase(it);
2580 +}
2581 +
2582 +void WSLCSession::CancelUserHandleIO()
2583 +{
2584 + for (auto handle : m_userHandles)
2585 + {
2586 + // Cancel all IO on the handle.
2587 + // N.B. This only cancels IO happening in this process.
2588 + if (!CancelIoEx(handle, nullptr))
2589 + {
2590 + LOG_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND);
2591 + }
2592 + }
2593 +}
2594 +
2595 +UserCOMCallback WSLCSession::RegisterUserCOMCallback()
2596 +{
2597 + std::lock_guard lock(m_userCOMCallbacksLock);
2598 +
2599 + // Don't allow new COM calls if the session is terminating.
2600 + // N.B. This check must happen under m_userCOMCallbacksLock to synchronize with Terminate().
2601 + THROW_HR_IF_MSG(
2602 + E_ABORT, m_sessionTerminatingEvent.is_signaled(), "Refusing to make a COM callback while the session is terminating.");
2603 +
2604 + THROW_IF_FAILED(CoEnableCallCancellation(nullptr));
2605 +
2606 + auto [_, inserted] = m_userCOMCallbackThreads.insert(GetCurrentThreadId());
2607 + WI_VERIFY(inserted);
2608 +
2609 + return UserCOMCallback{*this};
2610 +}
2611 +
2612 +void WSLCSession::UnregisterUserCOMCallback(DWORD ThreadId)
2613 +{
2614 + std::lock_guard lock(m_userCOMCallbacksLock);
2615 +
2616 + auto it = m_userCOMCallbackThreads.find(ThreadId);
2617 + WI_VERIFY(it != m_userCOMCallbackThreads.end());
2618 +
2619 + m_userCOMCallbackThreads.erase(it);
2620 +}
2621 +
2622 +void WSLCSession::CancelUserCOMCallbacks()
2623 +{
2624 + for (auto threadId : m_userCOMCallbackThreads)
2625 + {
2626 + LOG_IF_FAILED(CoCancelCall(threadId, 0));
2627 + }
2628 +}
2629 +
2630 +void WSLCSession::OnContainerDeleted(const WSLCContainerImpl* Container)
2631 +{
2632 + auto lock = m_lock.lock_shared();
2633 + std::lock_guard containersLock(m_containersLock);
2634 +
2635 + WI_VERIFY(std::erase_if(m_containers, [Container](const auto& e) { return e.get() == Container; }) == 1);
2636 +}
2637 +
2638 +HRESULT WSLCSession::GetState(_Out_ WSLCSessionState* State)
2639 +{
2640 + *State = m_terminated ? WSLCSessionStateTerminated : WSLCSessionStateRunning;
2641 + return S_OK;
2642 +}
2643 +
2644 +void WSLCSession::RecoverExistingContainers()
2645 +{
2646 + WI_ASSERT(m_dockerClient.has_value());
2647 + WI_ASSERT(m_eventTracker.has_value());
2648 + WI_ASSERT(m_virtualMachine.has_value());
2649 +
2650 + auto containers = m_dockerClient->ListContainers(true); // all=true to include stopped containers
2651 +
2652 + for (const auto& dockerContainer : containers)
2653 + {
2654 + try
2655 + {
2656 + auto container = WSLCContainerImpl::Open(
2657 + dockerContainer,
2658 + *this,
2659 + m_virtualMachine.value(),
2660 + m_volumes,
2661 + m_anonymousVolumes,
2662 + std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
2663 + m_eventTracker.value(),
2664 + m_dockerClient.value(),
2665 + m_ioRelay);
2666 +
2667 + m_containers.emplace_back(std::move(container));
2668 + }
2669 + catch (...)
2670 + {
2671 + // Log but don't fail the session startup if a single container fails to recover.
2672 + LOG_CAUGHT_EXCEPTION_MSG("Failed to recover container: %hs", dockerContainer.Id.c_str());
2673 + }
2674 + }
2675 +
2676 + WSL_LOG(
2677 + "ContainersRecovered",
2678 + TraceLoggingValue(m_displayName.c_str(), "SessionName"),
2679 + TraceLoggingValue(m_containers.size(), "ContainerCount"));
2680 +}
2681 +
2682 +void WSLCSession::RecoverExistingNetworks()
2683 +{
2684 + WI_ASSERT(m_dockerClient.has_value());
2685 + WI_ASSERT(m_virtualMachine.has_value());
2686 +
2687 + auto networks = m_dockerClient->ListNetworks();
2688 +
2689 + std::lock_guard networksLock(m_networksLock);
2690 +
2691 + for (const auto& network : networks)
2692 + {
2693 + if (!network.Labels.contains(WSLCNetworkManagedLabel))
2694 + {
2695 + continue;
2696 + }
2697 +
2698 + try
2699 + {
2700 + WI_ASSERT(!m_networks.contains(network.Name));
2701 +
2702 + NetworkEntry entry;
2703 + entry.Id = network.Id;
2704 + entry.Driver = network.Driver;
2705 + entry.Scope = network.Scope;
2706 + entry.Internal = network.Internal;
2707 + entry.Labels = network.Labels;
2708 + entry.IPAM.Driver = network.IPAM.Driver;
2709 + if (network.IPAM.Config)
2710 + {
2711 + auto& cfgs = entry.IPAM.Config.emplace();
2712 + for (const auto& c : *network.IPAM.Config)
2713 + {
2714 + cfgs.push_back({c.Subnet, c.Gateway});
2715 + }
2716 + }
2717 +
2718 + auto [_, inserted] = m_networks.insert({network.Name, std::move(entry)});
2719 + WI_VERIFY(inserted);
2720 + }
2721 + CATCH_LOG_MSG("Failed to recover network: %hs", network.Name.c_str());
2722 + }
2723 +
2724 + WSL_LOG(
2725 + "NetworksRecovered",
2726 + TraceLoggingValue(m_displayName.c_str(), "SessionName"),
2727 + TraceLoggingValue(m_networks.size(), "NetworkCount"));
2728 +}
2729 +
2730 +void WSLCSession::RecoverExistingVolumes()
2731 +{
2732 + WI_ASSERT(m_dockerClient.has_value());
2733 + WI_ASSERT(m_virtualMachine.has_value());
2734 +
2735 + auto volumes = m_dockerClient->ListVolumes();
2736 +
2737 + std::lock_guard volumesLock(m_volumesLock);
2738 +
2739 + for (const auto& volume : volumes)
2740 + {
2741 + if (!volume.Labels.has_value() || !volume.Labels->contains(WSLCVolumeMetadataLabel))
2742 + {
2743 + m_anonymousVolumes.insert(volume.Name);
2744 + continue;
2745 + }
2746 +
2747 + try
2748 + {
2749 + WI_ASSERT(!m_volumes.contains(volume.Name));
2750 +
2751 + // Peek at the driver field to decide which implementation to use.
2752 + const auto& metadataJson = volume.Labels->at(WSLCVolumeMetadataLabel);
2753 + auto metadata = wsl::shared::FromJson<WSLCVolumeMetadata>(metadataJson.c_str());
2754 +
2755 + std::unique_ptr<IWSLCVolume> recovered;
2756 + if (metadata.Driver == WSLCVhdVolumeDriver)
2757 + {
2758 + recovered = WSLCVhdVolumeImpl::Open(volume, m_virtualMachine.value(), m_dockerClient.value());
2759 + }
2760 + else if (metadata.Driver == WSLCGuestVolumeDriver)
2761 + {
2762 + recovered = WSLCGuestVolumeImpl::Open(volume, m_dockerClient.value());
2763 + }
2764 + else
2765 + {
2766 + WSL_LOG(
2767 + "VolumeRecoverySkippedUnknownDriver",
2768 + TraceLoggingValue(volume.Name.c_str(), "VolumeName"),
2769 + TraceLoggingValue(metadata.Driver.c_str(), "Driver"));
2770 + continue;
2771 + }
2772 +
2773 + auto [_, inserted] = m_volumes.insert({volume.Name, std::move(recovered)});
2774 + WI_VERIFY(inserted);
2775 + }
2776 + CATCH_LOG_MSG("Failed to recover volume: %hs", volume.Name.c_str());
2777 + }
2778 +
2779 + WSL_LOG(
2780 + "VolumesRecovered",
2781 + TraceLoggingValue(m_displayName.c_str(), "SessionName"),
2782 + TraceLoggingValue(m_volumes.size(), "VolumeCount"));
2783 +}
2784 +
2785 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCSession.h new
+235
@@ -0,0 +1,235 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSession.h
8 +
9 +Abstract:
10 +
11 + TODO
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "wslc.h"
18 +#include "WSLCVirtualMachine.h"
19 +#include "WSLCContainer.h"
20 +#include "IWSLCVolume.h"
21 +#include "WSLCVhdVolume.h"
22 +#include "WSLCGuestVolume.h"
23 +#include "WSLCVolumeMetadata.h"
24 +#include "WSLCNetworkMetadata.h"
25 +#include "ContainerEventTracker.h"
26 +#include "DockerHTTPClient.h"
27 +#include "IORelay.h"
28 +#include <unordered_map>
29 +
30 +namespace wsl::windows::service::wslc {
31 +
32 +class WSLCSession;
33 +
34 +class UserHandle
35 +{
36 + NON_COPYABLE(UserHandle);
37 +
38 +public:
39 + UserHandle(WSLCSession& Session, HANDLE handle);
40 + UserHandle(UserHandle&& Other);
41 +
42 + ~UserHandle();
43 +
44 + UserHandle& operator=(UserHandle&& Other);
45 +
46 + HANDLE Get() const noexcept;
47 + void Reset();
48 +
49 +private:
50 + WSLCSession* m_session{};
51 + HANDLE m_handle{};
52 +};
53 +
54 +class UserCOMCallback
55 +{
56 + NON_COPYABLE(UserCOMCallback);
57 +
58 +public:
59 + UserCOMCallback(WSLCSession& Session) noexcept;
60 + UserCOMCallback(UserCOMCallback&& Other) noexcept;
61 +
62 + ~UserCOMCallback() noexcept;
63 +
64 + UserCOMCallback& operator=(UserCOMCallback&& Other) noexcept;
65 + void Reset() noexcept;
66 +
67 +private:
68 + WSLCSession* m_session{};
69 + DWORD m_threadId{};
70 +};
71 +
72 +//
73 +// WSLCSession - Implements IWSLCSession for container management.
74 +// Runs in a per-user COM server process for security isolation.
75 +// The SYSTEM service creates the VM and passes IWSLCVirtualMachine to Initialize().
76 +//
77 +class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession
78 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::WinRtClassicComMix>, IWSLCSession, IFastRundown, ISupportErrorInfo>
79 +{
80 +public:
81 + WSLCSession() = default;
82 +
83 + ~WSLCSession();
84 +
85 + // Sets a callback invoked when this object is destroyed.
86 + // Used by the COM server host to signal process exit.
87 + void SetDestructionCallback(std::function<void()>&& callback);
88 +
89 + // IWSLCSession - initialization methods
90 + IFACEMETHOD(GetProcessHandle)(_Out_ HANDLE* ProcessHandle) override;
91 + IFACEMETHOD(Initialize)(_In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm) override;
92 +
93 + IFACEMETHOD(GetId)(_Out_ ULONG* Id) override;
94 + IFACEMETHOD(GetState)(_Out_ WSLCSessionState* State) override;
95 +
96 + // Image management.
97 + IFACEMETHOD(PullImage)(_In_ LPCSTR Image, _In_opt_ LPCSTR RegistryAuthenticationInformation, _In_opt_ IProgressCallback* ProgressCallback) override;
98 + IFACEMETHOD(BuildImage)(_In_ const WSLCBuildImageOptions* Options, _In_opt_ IProgressCallback* ProgressCallback, _In_opt_ HANDLE CancelEvent) override;
99 + IFACEMETHOD(LoadImage)(_In_ const WSLCHandle ImageHandle, _In_ IProgressCallback* ProgressCallback, _In_ ULONGLONG ContentLength) override;
100 + IFACEMETHOD(ImportImage)(_In_ const WSLCHandle ImageHandle, _In_ LPCSTR ImageName, _In_ IProgressCallback* ProgressCallback, _In_ ULONGLONG ContentLength) override;
101 + IFACEMETHOD(SaveImage)(_In_ WSLCHandle OutputHandle, _In_ LPCSTR ImageNameOrID, _In_ IProgressCallback* ProgressCallback, _In_opt_ HANDLE CancelEvent) override;
102 + IFACEMETHOD(ListImages)(_In_opt_ const WSLCListImageOptions* Options, _Out_ WSLCImageInformation** Images, _Out_ ULONG* Count) override;
103 + IFACEMETHOD(DeleteImage)(_In_ const WSLCDeleteImageOptions* Options, _Out_ WSLCDeletedImageInformation** DeletedImages, _Out_ ULONG* Count) override;
104 + IFACEMETHOD(TagImage)(_In_ const WSLCTagImageOptions* Options) override;
105 + IFACEMETHOD(PushImage)(_In_ LPCSTR Image, _In_ LPCSTR RegistryAuthenticationInformation, _In_opt_ IProgressCallback* ProgressCallback) override;
106 + IFACEMETHOD(InspectImage)(_In_ LPCSTR ImageNameOrId, _Out_ LPSTR* Output) override;
107 + IFACEMETHOD(Authenticate)(_In_ LPCSTR ServerAddress, _In_ LPCSTR Username, _In_ LPCSTR Password, _Out_ LPSTR* IdentityToken) override;
108 + IFACEMETHOD(PruneImages)(
109 + _In_opt_ const WSLCPruneImagesOptions* Options,
110 + _Out_ WSLCDeletedImageInformation** DeletedImages,
111 + _Out_ ULONG* DeletedImagesCount,
112 + _Out_ ULONGLONG* SpaceReclaimed) override;
113 +
114 + // Container management.
115 + IFACEMETHOD(CreateContainer)(_In_ const WSLCContainerOptions* Options, _Out_ IWSLCContainer** Container) override;
116 + IFACEMETHOD(OpenContainer)(_In_ LPCSTR Id, _In_ IWSLCContainer** Container) override;
117 + IFACEMETHOD(ListContainers)(_Out_ WSLCContainerEntry** Containers, _Out_ ULONG* Count, _Out_ WSLCContainerPortMapping** Ports, _Out_ ULONG* PortsCount) override;
118 + IFACEMETHOD(PruneContainers)(_In_opt_ WSLCPruneLabelFilter* Filters, _In_ DWORD FiltersCount, _In_ ULONGLONG Until, _Out_ WSLCPruneContainersResults* Result) override;
119 +
120 + // VM management.
121 + IFACEMETHOD(CreateRootNamespaceProcess)(
122 + _In_ LPCSTR Executable, _In_ const WSLCProcessOptions* Options, _Out_ IWSLCProcess** VirtualMachine, _Out_ int* Errno) override;
123 +
124 + // Disk management.
125 + IFACEMETHOD(FormatVirtualDisk)(_In_ LPCWSTR Path) override;
126 +
127 + // Volume management.
128 + IFACEMETHOD(CreateVolume)(_In_ const WSLCVolumeOptions* Options, _Out_ WSLCVolumeInformation* VolumeInfo) override;
129 + IFACEMETHOD(DeleteVolume)(_In_ LPCSTR Name) override;
130 + IFACEMETHOD(ListVolumes)(_Out_ WSLCVolumeInformation** Volumes, _Out_ ULONG* Count) override;
131 + IFACEMETHOD(InspectVolume)(_In_ LPCSTR Name, _Out_ LPSTR* Output) override;
132 + IFACEMETHOD(PruneVolumes)(_In_opt_ const WSLCPruneVolumesOptions* Options, _Out_ WSLCPruneVolumesResults* Results) override;
133 +
134 + // Network management.
135 + IFACEMETHOD(CreateNetwork)(_In_ const WSLCNetworkOptions* Options) override;
136 + IFACEMETHOD(DeleteNetwork)(_In_ LPCSTR Name) override;
137 + IFACEMETHOD(ListNetworks)(_Out_ WSLCNetworkInformation** Networks, _Out_ ULONG* Count) override;
138 + IFACEMETHOD(InspectNetwork)(_In_ LPCSTR Name, _Out_ LPSTR* Output) override;
139 +
140 + IFACEMETHOD(Terminate()) override;
141 +
142 + // ISupportErrorInfo
143 + IFACEMETHOD(InterfaceSupportsErrorInfo)(_In_ REFIID riid) override;
144 +
145 + // Testing.
146 + IFACEMETHOD(MountWindowsFolder)(_In_ LPCWSTR WindowsPath, _In_ LPCSTR LinuxPath, _In_ BOOL ReadOnly) override;
147 + IFACEMETHOD(UnmountWindowsFolder)(_In_ LPCSTR LinuxPath) override;
148 + IFACEMETHOD(MapVmPort)(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort) override;
149 + IFACEMETHOD(UnmapVmPort)(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort) override;
150 +
151 + common::relay::MultiHandleWait CreateIOContext(HANDLE CancelHandle = nullptr);
152 +
153 + UserHandle OpenUserHandle(WSLCHandle Handle);
154 + void ReleaseUserHandle(HANDLE Handle);
155 +
156 + UserCOMCallback RegisterUserCOMCallback();
157 + void UnregisterUserCOMCallback(DWORD ThreadId);
158 +
159 + HANDLE SessionTerminatingEvent() const noexcept
160 + {
161 + return m_sessionTerminatingEvent.get();
162 + }
163 +
164 + ULONG Id() const noexcept
165 + {
166 + return m_id;
167 + }
168 +
169 +private:
170 + ULONG m_id = 0;
171 +
172 + __requires_lock_held(m_userHandlesLock) void CancelUserHandleIO();
173 + __requires_lock_held(m_userCOMCallbacksLock) void CancelUserCOMCallbacks();
174 + void ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid);
175 + void Ext4Format(const std::string& Device);
176 + void OnContainerDeleted(const WSLCContainerImpl* Container);
177 + void OnProcessLog(const gsl::span<char>& Data, PCSTR Source);
178 + void OnContainerdExited();
179 + void OnDockerdExited();
180 + void OnVmExited();
181 + ServiceRunningProcess StartProcess(
182 + const std::string& Executable, const std::vector<std::string>& Args, PCSTR LogSource, std::function<void()>&& ExitCallback);
183 + void StartContainerd();
184 + void StartDockerd();
185 + int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs);
186 + void ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle);
187 + void RecoverExistingContainers();
188 + void RecoverExistingVolumes();
189 + void RecoverExistingNetworks();
190 +
191 + void SaveImageImpl(std::pair<uint32_t, wil::unique_socket>& RequestCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent);
192 + void StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback);
193 +
194 + std::optional<DockerHTTPClient> m_dockerClient;
195 + std::optional<WSLCVirtualMachine> m_virtualMachine;
196 + std::optional<ContainerEventTracker> m_eventTracker;
197 + wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset};
198 + std::wstring m_displayName;
199 + std::filesystem::path m_storageVhdPath;
200 +
201 + // N.B. m_lock must be acquired before acquiring m_volumesLock, m_containersLock, or m_networksLock.
202 + // These locks protect m_volumes / m_containers without requiring an exclusive m_lock.
203 + // This allows independent operations to proceed while volume/container bookkeeping remains synchronized.
204 + std::mutex m_containersLock;
205 + std::mutex m_volumesLock;
206 + std::vector<std::unique_ptr<WSLCContainerImpl>> m_containers;
207 + std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>> m_volumes;
208 + std::unordered_set<std::string> m_anonymousVolumes; // TODO: Implement proper anonymous volume support.
209 + std::mutex m_networksLock;
210 + std::unordered_map<std::string, NetworkEntry> m_networks;
211 + wil::unique_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset};
212 + wil::unique_event m_vmExitedEvent;
213 + wil::srwlock m_lock;
214 + IORelay m_ioRelay;
215 + std::optional<ServiceRunningProcess> m_containerdProcess;
216 + std::optional<ServiceRunningProcess> m_dockerdProcess;
217 + WSLCFeatureFlags m_featureFlags{};
218 + std::function<void()> m_destructionCallback;
219 + std::atomic<bool> m_terminating{false};
220 + std::atomic<bool> m_terminated{false};
221 +
222 + // User-provided handles that the session is currently doing IO on.
223 + std::mutex m_userHandlesLock;
224 + __guarded_by(m_userHandlesLock) std::vector<HANDLE> m_userHandles;
225 +
226 + // Threads currently inside an outgoing COM callback (e.g. IProgressCallback::OnProgress).
227 + std::recursive_mutex m_userCOMCallbacksLock;
228 + __guarded_by(m_userCOMCallbacksLock) std::set<DWORD> m_userCOMCallbackThreads;
229 +
230 + // Used for testing only.
231 + std::mutex m_allocatedPortsLock;
232 + __guarded_by(m_allocatedPortsLock) std::map<uint16_t, std::pair<std::shared_ptr<VmPortAllocation>, size_t>> m_allocatedPorts;
233 +};
234 +
235 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCSessionFactory.cpp new
+75
@@ -0,0 +1,75 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionFactory.cpp
8 +
9 +Abstract:
10 +
11 + Implementation for WSLCSessionFactory.
12 +
13 + Creates WSLCSession objects in the per-user COM server process along with
14 + their corresponding IWSLCSessionReference weak references for the SYSTEM
15 + service to track session lifetime.
16 +
17 +--*/
18 +
19 +#include "WSLCSessionFactory.h"
20 +#include "WSLCSession.h"
21 +#include "WSLCSessionReference.h"
22 +#include "wslutil.h"
23 +
24 +namespace wslutil = wsl::windows::common::wslutil;
25 +namespace wslc = wsl::windows::service::wslc;
26 +
27 +void wslc::WSLCSessionFactory::SetDestructionCallback(std::function<void()>&& callback)
28 +{
29 + m_destructionCallback = std::move(callback);
30 +}
31 +
32 +HRESULT wslc::WSLCSessionFactory::CreateSession(
33 + _In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm, _Out_ IWSLCSession** Session, _Out_ IWSLCSessionReference** ServiceRef)
34 +try
35 +{
36 + *Session = nullptr;
37 + *ServiceRef = nullptr;
38 +
39 + // Create the session object.
40 + auto session = Microsoft::WRL::Make<wslc::WSLCSession>();
41 +
42 + // Pass the destruction callback directly to the session.
43 + // One session per process, so when it's destroyed, exit.
44 + session->SetDestructionCallback(std::move(m_destructionCallback));
45 +
46 + // Initialize the session with the VM.
47 + RETURN_IF_FAILED(session->Initialize(Settings, Vm));
48 +
49 + // Create the service session ref. It extracts metadata and a weak reference from the session.
50 + auto serviceRef = Microsoft::WRL::Make<wslc::WSLCSessionReference>(session.Get());
51 +
52 + // Return the session as IWSLCSession interface
53 + RETURN_IF_FAILED(session->QueryInterface(IID_PPV_ARGS(Session)));
54 + *ServiceRef = serviceRef.Detach();
55 +
56 + WSL_LOG(
57 + "WSLCSessionFactoryCreatedSession",
58 + TraceLoggingLevel(WINEVENT_LEVEL_INFO),
59 + TraceLoggingUInt32(Settings->SessionId, "SessionId"),
60 + TraceLoggingWideString(Settings->DisplayName, "DisplayName"));
61 +
62 + return S_OK;
63 +}
64 +CATCH_RETURN()
65 +
66 +HRESULT wslc::WSLCSessionFactory::GetProcessHandle(_Out_ HANDLE* ProcessHandle)
67 +try
68 +{
69 + wil::unique_handle process{OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, FALSE, GetCurrentProcessId())};
70 + RETURN_LAST_ERROR_IF(!process);
71 +
72 + *ProcessHandle = process.release();
73 + return S_OK;
74 +}
75 +CATCH_RETURN()
src/windows/wslcsession/WSLCSessionFactory.h new
+56
@@ -0,0 +1,56 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionFactory.h
8 +
9 +Abstract:
10 +
11 + IWSLCSessionFactory implementation.
12 +
13 + This factory runs in the per-user COM server process and is created by
14 + the SYSTEM service via CoCreateInstanceAsUser. It creates WSLCSession
15 + objects and their corresponding IWSLCSessionReference weak references.
16 +
17 + The factory is responsible for:
18 + - Creating the WSLCSession in the per-user security context
19 + - Creating the IWSLCSessionReference that holds a weak reference
20 + - Providing the process handle for job object management
21 +
22 +--*/
23 +
24 +#pragma once
25 +#include "wslc.h"
26 +#include <wrl/implements.h>
27 +#include <wil/com.h>
28 +#include <functional>
29 +
30 +namespace wsl::windows::service::wslc {
31 +
32 +class DECLSPEC_UUID("9FCD2067-9FC6-4EFA-9EB0-698169EBF7D3") WSLCSessionFactory
33 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCSessionFactory, IFastRundown>
34 +{
35 +public:
36 + NON_COPYABLE(WSLCSessionFactory);
37 + NON_MOVABLE(WSLCSessionFactory);
38 +
39 + WSLCSessionFactory() = default;
40 +
41 + // Sets a callback invoked when the session in this process is destroyed.
42 + // Used by the COM server host to signal process exit.
43 + void SetDestructionCallback(std::function<void()>&& callback);
44 +
45 + // IWSLCSessionFactory
46 + IFACEMETHOD(CreateSession)
47 + (_In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm, _Out_ IWSLCSession** Session, _Out_ IWSLCSessionReference** ServiceRef)
48 + override;
49 +
50 + IFACEMETHOD(GetProcessHandle)(_Out_ HANDLE* ProcessHandle) override;
51 +
52 +private:
53 + std::function<void()> m_destructionCallback;
54 +};
55 +
56 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCSessionReference.cpp new
+67
@@ -0,0 +1,67 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionReference.cpp
8 +
9 +Abstract:
10 +
11 + Implementation for WSLCSessionReference.
12 +
13 + This class provides a weak reference to a session that the SYSTEM service
14 + can use to:
15 + - Check if a session is still alive (OpenSession fails if session is gone)
16 + - Terminate sessions when requested by elevated callers
17 +
18 +--*/
19 +
20 +#include "WSLCSessionReference.h"
21 +#include "WSLCSession.h"
22 +
23 +namespace wslc = wsl::windows::service::wslc;
24 +
25 +wslc::WSLCSessionReference::WSLCSessionReference(_In_ WSLCSession* Session)
26 +{
27 + Microsoft::WRL::ComPtr<IWeakReferenceSource> weakRefSource;
28 + THROW_IF_FAILED(Session->QueryInterface(IID_PPV_ARGS(&weakRefSource)));
29 + THROW_IF_FAILED(weakRefSource->GetWeakReference(&m_weakSession));
30 +}
31 +
32 +wslc::WSLCSessionReference::~WSLCSessionReference() = default;
33 +
34 +HRESULT wslc::WSLCSessionReference::OpenSession(_Out_ IWSLCSession** Session)
35 +{
36 + *Session = nullptr;
37 +
38 + Microsoft::WRL::ComPtr<IWSLCSession> lockedSession;
39 + RETURN_IF_FAILED(m_weakSession->Resolve(__uuidof(IWSLCSession), reinterpret_cast<IInspectable**>(lockedSession.GetAddressOf())));
40 +
41 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_OBJECT_NO_LONGER_EXISTS), !lockedSession);
42 +
43 + WSLCSessionState state{};
44 + RETURN_IF_FAILED(lockedSession->GetState(&state));
45 +
46 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), state != WSLCSessionStateRunning);
47 +
48 + *Session = lockedSession.Detach();
49 + return S_OK;
50 +}
51 +
52 +HRESULT wslc::WSLCSessionReference::Terminate()
53 +try
54 +{
55 + // Resolve the weak reference directly (bypassing OpenSession which checks GetState).
56 + // We want to terminate regardless of session state.
57 + Microsoft::WRL::ComPtr<IWSLCSession> session;
58 + RETURN_IF_FAILED(m_weakSession->Resolve(__uuidof(IWSLCSession), reinterpret_cast<IInspectable**>(session.GetAddressOf())));
59 +
60 + if (session)
61 + {
62 + return session->Terminate();
63 + }
64 +
65 + return S_OK; // Session already released
66 +}
67 +CATCH_RETURN()
src/windows/wslcsession/WSLCSessionReference.h new
+53
@@ -0,0 +1,53 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionReference.h
8 +
9 +Abstract:
10 +
11 + IWSLCSessionReference implementation.
12 +
13 + This object lives in the per-user COM server process and holds a weak
14 + reference to the WSLCSession. The SYSTEM service holds these references
15 + to track active sessions without preventing session cleanup when clients
16 + release their references.
17 +
18 + When OpenSession() is called:
19 + - If the session is still alive, it returns S_OK with a strong reference
20 + - If the session has been released, it returns ERROR_OBJECT_NO_LONGER_EXISTS
21 + - If the session has been terminated, it returns ERROR_INVALID_STATE
22 +
23 +--*/
24 +
25 +#pragma once
26 +#include "wslc.h"
27 +#include <wrl/implements.h>
28 +#include <wil/com.h>
29 +
30 +namespace wsl::windows::service::wslc {
31 +
32 +class WSLCSession;
33 +
34 +class WSLCSessionReference
35 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCSessionReference, IFastRundown, Microsoft::WRL::FtmBase>
36 +{
37 +public:
38 + NON_COPYABLE(WSLCSessionReference);
39 + NON_MOVABLE(WSLCSessionReference);
40 +
41 + WSLCSessionReference(_In_ WSLCSession* Session);
42 +
43 + ~WSLCSessionReference();
44 +
45 + // IWSLCSessionReference
46 + IFACEMETHOD(OpenSession)(_Out_ IWSLCSession** Session) override;
47 + IFACEMETHOD(Terminate)() override;
48 +
49 +private:
50 + Microsoft::WRL::ComPtr<IWeakReference> m_weakSession;
51 +};
52 +
53 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCVhdVolume.cpp new
+281
@@ -0,0 +1,281 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCVhdVolume.cpp
8 +
9 +Abstract:
10 +
11 + Internal implementation for VHD-backed named volumes.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "DockerHTTPClient.h"
17 +#include "WSLCVhdVolume.h"
18 +#include "WSLCVirtualMachine.h"
19 +#include "WSLCVolumeMetadata.h"
20 +#include "WslCoreFilesystem.h"
21 +#include "wslc_schema.h"
22 +
23 +using namespace wsl::windows::common;
24 +using wsl::shared::Localization;
25 +
26 +namespace wsl::windows::service::wslc {
27 +
28 +namespace {
29 + std::string GenerateName()
30 + {
31 + std::random_device rd;
32 + std::independent_bits_engine<std::default_random_engine, CHAR_BIT, unsigned short> random(rd());
33 +
34 + std::array<unsigned short, 32> randomBytes;
35 + std::generate(randomBytes.begin(), randomBytes.end(), random);
36 +
37 + std::string name;
38 + name.reserve(randomBytes.size() * 2);
39 + for (auto b : randomBytes)
40 + {
41 + std::format_to(std::back_inserter(name), "{:02x}", static_cast<BYTE>(b));
42 + }
43 +
44 + return name;
45 + }
46 +
47 + ULONGLONG ParseSizeBytes(std::map<std::string, std::string>& DriverOpts)
48 + {
49 + const auto it = DriverOpts.find("SizeBytes");
50 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcMissingVolumeOption("SizeBytes"), it == DriverOpts.end());
51 +
52 + auto& value = it->second;
53 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageInvalidSize(value), value.empty() || value[0] == '-');
54 +
55 + errno = 0;
56 + char* end = nullptr;
57 + auto sizeBytes = wsl::shared::string::ToUInt64(value.c_str(), &end);
58 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageInvalidSize(value), errno != 0 || *end != '\0' || sizeBytes == 0);
59 +
60 + return sizeBytes;
61 + }
62 +
63 +} // namespace
64 +
65 +WSLCVhdVolumeImpl::WSLCVhdVolumeImpl(
66 + std::string&& Name,
67 + std::filesystem::path&& HostPath,
68 + ULONGLONG SizeBytes,
69 + ULONG Lun,
70 + std::string&& VirtualMachinePath,
71 + std::map<std::string, std::string>&& DriverOpts,
72 + std::map<std::string, std::string>&& Labels,
73 + WSLCVirtualMachine& VirtualMachine,
74 + DockerHTTPClient& DockerClient) :
75 + m_name(std::move(Name)),
76 + m_hostPath(std::move(HostPath)),
77 + m_virtualMachinePath(std::move(VirtualMachinePath)),
78 + m_driverOpts(std::move(DriverOpts)),
79 + m_labels(std::move(Labels)),
80 + m_sizeBytes(SizeBytes),
81 + m_lun(Lun),
82 + m_virtualMachine(VirtualMachine),
83 + m_dockerClient(DockerClient)
84 +{
85 +}
86 +
87 +WSLCVhdVolumeImpl::~WSLCVhdVolumeImpl()
88 +{
89 + Detach();
90 +}
91 +
92 +std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Create(
93 + LPCSTR Name,
94 + std::map<std::string, std::string>&& DriverOpts,
95 + std::map<std::string, std::string>&& Labels,
96 + const std::filesystem::path& StoragePath,
97 + WSLCVirtualMachine& VirtualMachine,
98 + DockerHTTPClient& DockerClient)
99 +{
100 + std::string name = (Name != nullptr && Name[0] != '\0') ? std::string(Name) : GenerateName();
101 + auto sizeBytes = ParseSizeBytes(DriverOpts);
102 + auto hostPath = StoragePath / "volumes" / (name + ".vhdx");
103 +
104 + auto createVhdCleanup =
105 + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(hostPath.c_str())); });
106 +
107 + std::filesystem::create_directories(hostPath.parent_path());
108 +
109 + const auto tokenInfo = wil::get_token_information<TOKEN_USER>(GetCurrentProcessToken());
110 + wsl::core::filesystem::CreateVhd(hostPath.c_str(), sizeBytes, tokenInfo->User.Sid, false, false);
111 +
112 + auto [lun, device] = VirtualMachine.AttachDisk(hostPath.c_str(), false);
113 + auto attachCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.DetachDisk(lun); });
114 +
115 + VirtualMachine.Ext4Format(device);
116 +
117 + auto virtualMachinePath = std::format("/mnt/wslc-volumes/{}", name);
118 + VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "", 0);
119 +
120 + auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
121 +
122 + WSLCVolumeMetadata metadata;
123 + metadata.Driver = WSLCVhdVolumeDriver;
124 + metadata.DriverOpts = DriverOpts;
125 + metadata.Properties = {
126 + {"HostPath", hostPath.string()},
127 + };
128 +
129 + docker_schema::CreateVolume request{};
130 + request.Name = name;
131 + request.Driver = "local";
132 + request.DriverOpts = {
133 + {"type", "none"},
134 + {"o", "bind"},
135 + {"device", virtualMachinePath},
136 + };
137 + request.Labels = {{WSLCVolumeMetadataLabel, wsl::shared::ToJson(metadata)}};
138 +
139 + // Merge user labels into the Docker volume labels.
140 + for (const auto& [key, value] : Labels)
141 + {
142 + request.Labels[key] = value;
143 + }
144 +
145 + try
146 + {
147 + auto createdVolume = DockerClient.CreateVolume(request);
148 +
149 + auto volume = std::make_unique<WSLCVhdVolumeImpl>(
150 + std::move(name), std::move(hostPath), sizeBytes, lun, std::move(virtualMachinePath), std::move(DriverOpts), std::move(Labels), VirtualMachine, DockerClient);
151 + volume->m_createdAt = createdVolume.CreatedAt;
152 +
153 + mountCleanup.release();
154 + attachCleanup.release();
155 + createVhdCleanup.release();
156 +
157 + return volume;
158 + }
159 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to create volume '%hs'", name.c_str());
160 +}
161 +
162 +std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Open(
163 + const wsl::windows::common::docker_schema::Volume& Volume, WSLCVirtualMachine& VirtualMachine, DockerHTTPClient& DockerClient)
164 +{
165 + THROW_HR_IF(E_INVALIDARG, !Volume.Labels.has_value());
166 +
167 + auto metadataIt = Volume.Labels->find(WSLCVolumeMetadataLabel);
168 + THROW_HR_IF(E_INVALIDARG, metadataIt == Volume.Labels->end());
169 +
170 + auto metadata = wsl::shared::FromJson<WSLCVolumeMetadata>(metadataIt->second.c_str());
171 + THROW_HR_IF(E_INVALIDARG, metadata.Driver != WSLCVhdVolumeDriver);
172 +
173 + auto hostPathIt = metadata.Properties.find("HostPath");
174 + THROW_HR_IF(E_INVALIDARG, hostPathIt == metadata.Properties.end());
175 + THROW_HR_IF(E_INVALIDARG, hostPathIt->second.empty());
176 +
177 + auto hostPath = std::filesystem::path(hostPathIt->second);
178 + auto driverOpts = metadata.DriverOpts;
179 + auto sizeBytes = ParseSizeBytes(driverOpts);
180 +
181 + THROW_HR_IF(E_INVALIDARG, !Volume.Options.has_value());
182 + auto deviceIt = Volume.Options->find("device");
183 + THROW_HR_IF(E_INVALIDARG, deviceIt == Volume.Options->end());
184 + THROW_HR_IF(E_INVALIDARG, deviceIt->second.empty());
185 + std::string virtualMachinePath = deviceIt->second;
186 +
187 + // Extract user labels (all labels except our internal metadata label).
188 + std::map<std::string, std::string> userLabels;
189 + for (const auto& [key, value] : *Volume.Labels)
190 + {
191 + if (key != WSLCVolumeMetadataLabel)
192 + {
193 + userLabels[key] = value;
194 + }
195 + }
196 +
197 + auto [lun, device] = VirtualMachine.AttachDisk(hostPath.c_str(), false);
198 + auto attachCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.DetachDisk(lun); });
199 +
200 + VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "", 0);
201 + auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
202 +
203 + auto volume = std::make_unique<WSLCVhdVolumeImpl>(
204 + std::string{Volume.Name}, std::move(hostPath), sizeBytes, lun, std::move(virtualMachinePath), std::move(driverOpts), std::move(userLabels), VirtualMachine, DockerClient);
205 + volume->m_createdAt = Volume.CreatedAt;
206 +
207 + mountCleanup.release();
208 + attachCleanup.release();
209 +
210 + return volume;
211 +}
212 +
213 +void WSLCVhdVolumeImpl::Delete()
214 +{
215 + try
216 + {
217 + m_dockerClient.RemoveVolume(m_name);
218 + }
219 + catch (const DockerHTTPException& e)
220 + {
221 + THROW_HR_WITH_USER_ERROR_IF(
222 + HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), Localization::MessageWslcVolumeInUse(m_name.c_str()), e.StatusCode() == 409);
223 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(m_name.c_str()), e.StatusCode() == 404);
224 + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to delete volume '%hs'", m_name.c_str());
225 + }
226 +
227 + OnDeleted();
228 +}
229 +
230 +std::string WSLCVhdVolumeImpl::Inspect() const
231 +{
232 + wslc_schema::InspectVolume inspect{};
233 + inspect.Name = m_name;
234 + inspect.Driver = WSLCVhdVolumeDriver;
235 + inspect.CreatedAt = m_createdAt;
236 + inspect.DriverOpts = m_driverOpts;
237 + inspect.Labels = m_labels;
238 + inspect.Status = std::map<std::string, std::string>{
239 + {"HostPath", m_hostPath.string()},
240 + {"SizeBytes", std::to_string(m_sizeBytes)},
241 + };
242 +
243 + return wsl::shared::ToJson(inspect);
244 +}
245 +
246 +WSLCVolumeInformation WSLCVhdVolumeImpl::GetVolumeInformation() const
247 +{
248 + WSLCVolumeInformation Info{};
249 +
250 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(Info.Name, m_name.c_str()) != 0);
251 + THROW_HR_IF(E_UNEXPECTED, strcpy_s(Info.Driver, WSLCVhdVolumeDriver) != 0);
252 +
253 + return Info;
254 +}
255 +
256 +void WSLCVhdVolumeImpl::OnDeleted()
257 +{
258 + Detach();
259 + LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_hostPath.c_str()));
260 +}
261 +
262 +void WSLCVhdVolumeImpl::Detach()
263 +try
264 +{
265 + if (!m_attached)
266 + {
267 + return;
268 + }
269 +
270 + if (!m_virtualMachinePath.empty())
271 + {
272 + m_virtualMachine.Unmount(m_virtualMachinePath.c_str());
273 + m_virtualMachinePath.clear();
274 + }
275 +
276 + m_virtualMachine.DetachDisk(m_lun);
277 + m_attached = false;
278 +}
279 +CATCH_LOG();
280 +
281 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCVhdVolume.h new
+98
@@ -0,0 +1,98 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCVhdVolume.h
8 +
9 +Abstract:
10 +
11 + Internal implementation for a VHD-backed volume.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "IWSLCVolume.h"
18 +#include "WSLCVolumeMetadata.h"
19 +#include "wslc.h"
20 +#include <filesystem>
21 +#include <memory>
22 +#include <string>
23 +
24 +namespace wsl::windows::common::docker_schema {
25 +struct Volume;
26 +}
27 +
28 +namespace wsl::windows::service::wslc {
29 +
30 +class WSLCVirtualMachine;
31 +class DockerHTTPClient;
32 +
33 +class WSLCVhdVolumeImpl : public IWSLCVolume
34 +{
35 +public:
36 + NON_COPYABLE(WSLCVhdVolumeImpl);
37 + NON_MOVABLE(WSLCVhdVolumeImpl);
38 +
39 + WSLCVhdVolumeImpl(
40 + std::string&& Name,
41 + std::filesystem::path&& HostPath,
42 + ULONGLONG SizeBytes,
43 + ULONG Lun,
44 + std::string&& VirtualMachinePath,
45 + std::map<std::string, std::string>&& DriverOpts,
46 + std::map<std::string, std::string>&& Labels,
47 + WSLCVirtualMachine& VirtualMachine,
48 + DockerHTTPClient& DockerClient);
49 +
50 + ~WSLCVhdVolumeImpl();
51 +
52 + static std::unique_ptr<WSLCVhdVolumeImpl> Create(
53 + _In_opt_ LPCSTR Name,
54 + _In_ std::map<std::string, std::string>&& DriverOpts,
55 + _In_ std::map<std::string, std::string>&& Labels,
56 + _In_ const std::filesystem::path& StoragePath,
57 + _In_ WSLCVirtualMachine& VirtualMachine,
58 + _In_ DockerHTTPClient& DockerClient);
59 +
60 + static std::unique_ptr<WSLCVhdVolumeImpl> Open(
61 + _In_ const wsl::windows::common::docker_schema::Volume& Volume, _In_ WSLCVirtualMachine& VirtualMachine, _In_ DockerHTTPClient& DockerClient);
62 +
63 + // IWSLCVolume
64 + const std::string& Name() const noexcept override
65 + {
66 + return m_name;
67 + }
68 + const char* Driver() const noexcept override
69 + {
70 + return WSLCVhdVolumeDriver;
71 + }
72 + void Delete() override;
73 + std::string Inspect() const override;
74 + WSLCVolumeInformation GetVolumeInformation() const override;
75 +
76 + const std::string& VirtualMachinePath() const noexcept
77 + {
78 + return m_virtualMachinePath;
79 + }
80 +
81 + void OnDeleted();
82 +
83 +private:
84 + void Detach();
85 + std::string m_name;
86 + std::filesystem::path m_hostPath;
87 + std::string m_virtualMachinePath;
88 + std::string m_createdAt;
89 + std::map<std::string, std::string> m_driverOpts;
90 + std::map<std::string, std::string> m_labels;
91 + ULONGLONG m_sizeBytes{};
92 + ULONG m_lun{};
93 + WSLCVirtualMachine& m_virtualMachine;
94 + DockerHTTPClient& m_dockerClient;
95 + bool m_attached{true};
96 +};
97 +
98 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCVirtualMachine.cpp new
+1269
@@ -0,0 +1,1269 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCVirtualMachine.cpp
8 +
9 +Abstract:
10 +
11 + Client-side class for WSLC virtual machine operations.
12 + The VM is created via IWSLCVirtualMachine (running in the SYSTEM service).
13 + This class connects to the existing VM for unprivileged operations
14 + and delegates privileged operations back to IWSLCVirtualMachine.
15 +
16 +--*/
17 +
18 +#include "WSLCVirtualMachine.h"
19 +#include <format>
20 +#include <filesystem>
21 +#include "ServiceProcessLauncher.h"
22 +#include "wslutil.h"
23 +#include "lxinitshared.h"
24 +
25 +using namespace wsl::windows::common;
26 +using wsl::windows::service::wslc::TypedHandle;
27 +using wsl::windows::service::wslc::VmPortAllocation;
28 +using wsl::windows::service::wslc::VMPortMapping;
29 +using wsl::windows::service::wslc::WSLCProcess;
30 +using wsl::windows::service::wslc::WSLCVirtualMachine;
31 +namespace wslutil = wsl::windows::common::wslutil;
32 +
33 +constexpr auto CONTAINER_PORT_RANGE = std::pair<uint16_t, uint16_t>(20002, 65535);
34 +
35 +static_assert(c_ephemeralPortRange.second < CONTAINER_PORT_RANGE.first);
36 +
37 +VmPortAllocation::VmPortAllocation(uint16_t port, int family, int protocol, WSLCVirtualMachine& vm) :
38 + m_port(port), m_family(family), m_protocol(protocol), m_vm(&vm)
39 +{
40 +}
41 +
42 +VmPortAllocation::VmPortAllocation(VmPortAllocation&& Other)
43 +{
44 + *this = std::move(Other);
45 +}
46 +
47 +VmPortAllocation& VmPortAllocation::operator=(VmPortAllocation&& Other)
48 +{
49 + if (this != &Other)
50 + {
51 + Reset();
52 + m_port = Other.m_port;
53 + m_family = Other.m_family;
54 + m_protocol = Other.m_protocol;
55 + m_vm = Other.m_vm;
56 +
57 + Other.Release();
58 + }
59 + return *this;
60 +}
61 +
62 +VmPortAllocation::~VmPortAllocation()
63 +{
64 + Reset();
65 +}
66 +
67 +void VmPortAllocation::Reset()
68 +{
69 + if (m_vm != nullptr)
70 + {
71 + m_vm->ReleasePort(*this);
72 + Release();
73 + }
74 +}
75 +
76 +void VmPortAllocation::Release()
77 +{
78 + m_vm = nullptr;
79 + m_port = 0;
80 + m_family = 0;
81 + m_protocol = 0;
82 +}
83 +
84 +uint16_t VmPortAllocation::Port() const
85 +{
86 + return m_port;
87 +}
88 +
89 +int VmPortAllocation::Family() const
90 +{
91 + return m_family;
92 +}
93 +
94 +int VmPortAllocation::Protocol() const
95 +{
96 + return m_protocol;
97 +}
98 +
99 +VMPortMapping::VMPortMapping(int protocol, int Family, uint16_t Port, const char* Address) : Protocol(protocol)
100 +{
101 + THROW_HR_IF_MSG(E_INVALIDARG, Protocol != IPPROTO_TCP && Protocol != IPPROTO_UDP, "Invalid protocol: %i", Protocol);
102 + THROW_HR_IF(E_POINTER, Address == nullptr);
103 + if (Family == AF_INET)
104 + {
105 + common::wslutil::ParseIpv4Address(Address, BindAddress.Ipv4.sin_addr);
106 + BindAddress.Ipv4.sin_port = htons(Port);
107 + }
108 + else if (Family == AF_INET6)
109 + {
110 + common::wslutil::ParseIpv6Address(Address, BindAddress.Ipv6.sin6_addr);
111 + BindAddress.Ipv6.sin6_port = htons(Port);
112 + }
113 + else
114 + {
115 + THROW_HR_MSG(E_INVALIDARG, "Invalid address family: %i", Family);
116 + }
117 +
118 + // Must be assigned after parsing is done, since inet_pton writes to the family field as well.
119 + BindAddress.si_family = Family;
120 +}
121 +
122 +VMPortMapping::~VMPortMapping()
123 +{
124 + try
125 + {
126 + Unmap();
127 + }
128 + CATCH_LOG();
129 +}
130 +
131 +VMPortMapping::VMPortMapping(VMPortMapping&& Other)
132 +{
133 + *this = std::move(Other);
134 +}
135 +
136 +void VMPortMapping::AssignVmPort(const std::shared_ptr<VmPortAllocation>& Port)
137 +{
138 + WI_ASSERT(!VmPort);
139 +
140 + VmPort = Port;
141 +}
142 +
143 +void VMPortMapping::Unmap()
144 +{
145 + if (Vm)
146 + {
147 + auto clearVm = wil::scope_exit([&] { Vm = nullptr; });
148 + Vm->UnmapPort(*this);
149 + }
150 +}
151 +
152 +void VMPortMapping::Release()
153 +{
154 + Vm = nullptr;
155 + VmPort.reset();
156 +}
157 +
158 +bool VMPortMapping::IsLocalhost() const
159 +{
160 + if (BindAddress.Ipv4.sin_family == AF_INET6)
161 + {
162 + return IN6_IS_ADDR_LOOPBACK(&BindAddress.Ipv6.sin6_addr);
163 + }
164 + else
165 + {
166 + return IN4ADDR_ISLOOPBACK(&BindAddress.Ipv4);
167 + }
168 +}
169 +
170 +bool VMPortMapping::IsIPv6() const
171 +{
172 + return BindAddress.si_family == AF_INET6;
173 +}
174 +
175 +uint16_t VMPortMapping::HostPort() const
176 +{
177 + if (BindAddress.si_family == AF_INET6)
178 + {
179 + return ntohs(BindAddress.Ipv6.sin6_port);
180 + }
181 + else
182 + {
183 + WI_ASSERT(BindAddress.si_family == AF_INET);
184 + return ntohs(BindAddress.Ipv4.sin_port);
185 + }
186 +}
187 +
188 +std::string VMPortMapping::BindingAddressString() const
189 +{
190 + char buffer[INET6_ADDRSTRLEN]{};
191 + if (BindAddress.Ipv4.sin_family == AF_INET6)
192 + {
193 + THROW_LAST_ERROR_IF(inet_ntop(AF_INET6, &BindAddress.Ipv6.sin6_addr, buffer, sizeof(buffer)) == nullptr);
194 + }
195 + else
196 + {
197 + THROW_LAST_ERROR_IF(inet_ntop(AF_INET, &BindAddress.Ipv4.sin_addr, buffer, sizeof(buffer)) == nullptr);
198 + }
199 +
200 + return buffer;
201 +}
202 +
203 +void VMPortMapping::Attach(WSLCVirtualMachine& Vm)
204 +{
205 + WI_ASSERT(this->Vm == nullptr);
206 +
207 + this->Vm = &Vm;
208 +}
209 +
210 +void VMPortMapping::Detach()
211 +{
212 + WI_ASSERT(Vm != nullptr);
213 +
214 + this->Vm = nullptr;
215 +}
216 +
217 +VMPortMapping VMPortMapping::LocalhostTcpMapping(int Family, uint16_t WindowsPort)
218 +{
219 + WI_ASSERT(Family == AF_INET || Family == AF_INET6);
220 +
221 + return VMPortMapping(IPPROTO_TCP, Family, WindowsPort, Family == AF_INET ? "127.0.0.1" : "::1");
222 +}
223 +
224 +VMPortMapping VMPortMapping::FromWSLCPortMapping(const ::WSLCPortMapping& Mapping)
225 +{
226 + return VMPortMapping(Mapping.Protocol, Mapping.Family, Mapping.HostPort, Mapping.BindingAddress);
227 +}
228 +
229 +VMPortMapping VMPortMapping::FromContainerMetaData(const wslc::WSLCPortMapping& Mapping)
230 +{
231 + return VMPortMapping(Mapping.Protocol, Mapping.Family, Mapping.HostPort, Mapping.BindingAddress.c_str());
232 +}
233 +
234 +VMPortMapping& VMPortMapping::operator=(VMPortMapping&& Other)
235 +{
236 + if (this != &Other)
237 + {
238 + Unmap();
239 + Protocol = Other.Protocol;
240 + VmPort = std::move(Other.VmPort);
241 + BindAddress = Other.BindAddress;
242 + Vm = Other.Vm;
243 +
244 + Other.Protocol = 0;
245 + ZeroMemory(&Other.BindAddress, sizeof(Other.BindAddress));
246 + Other.Vm = nullptr;
247 + }
248 + return *this;
249 +}
250 +
251 +WSLCVirtualMachine::WSLCVirtualMachine(_In_ IWSLCVirtualMachine* Vm, _In_ const WSLCSessionInitSettings* Settings) :
252 + m_vm(Vm),
253 + m_featureFlags(static_cast<WSLCFeatureFlags>(Settings->FeatureFlags)),
254 + m_networkingMode(Settings->NetworkingMode),
255 + m_bootTimeoutMs(Settings->BootTimeoutMs),
256 + m_rootVhdType(Settings->RootVhdTypeOverride ? Settings->RootVhdTypeOverride : "ext4")
257 +{
258 + // N.B. The constructor should not run any operation that could throw, so the destructor runs even if the VM fails to boot.
259 +}
260 +
261 +void WSLCVirtualMachine::Initialize()
262 +{
263 + THROW_IF_FAILED(m_vm->GetId(&m_vmId));
264 +
265 + // Start crash dump collection thread.
266 + auto crashDumpSocket = hvsocket::Listen(m_vmId, LX_INIT_UTILITY_VM_CRASH_DUMP_PORT);
267 + THROW_LAST_ERROR_IF(!crashDumpSocket);
268 +
269 + m_crashDumpThread = std::thread{[this, socket = std::move(crashDumpSocket)]() mutable { CollectCrashDumps(std::move(socket)); }};
270 +
271 + // Establish a socket channel with mini_init in the VM.
272 + wil::unique_socket socket;
273 + THROW_IF_FAILED(m_vm->AcceptConnection(reinterpret_cast<HANDLE*>(&socket)));
274 +
275 + m_initChannel = wsl::shared::SocketChannel{std::move(socket), "mini_init", m_vmTerminatingEvent.get()};
276 +
277 + // Create a thread to watch for exited processes.
278 + auto [__, ___, childChannel] = Fork(WSLC_FORK::Thread);
279 +
280 + WSLC_WATCH_PROCESSES watchMessage{};
281 + auto watchTransaction = childChannel.StartTransaction();
282 + watchTransaction.Send(watchMessage);
283 +
284 + THROW_HR_IF(E_FAIL, watchTransaction.Receive<RESULT_MESSAGE<uint32_t>>().Result != 0);
285 +
286 + m_processExitThread = std::thread(std::bind(&WSLCVirtualMachine::WatchForExitedProcesses, this, std::move(childChannel)));
287 +
288 + // Mount VHDs
289 + const auto rootDevice = GetVhdDevicePath(0);
290 + Mount(m_initChannel, rootDevice.c_str(), "/mnt", m_rootVhdType.c_str(), "ro", WSLC_MOUNT::Chroot | WSLC_MOUNT::OverlayFs);
291 +
292 + const auto modulesDevice = GetVhdDevicePath(1);
293 + Mount(m_initChannel, modulesDevice.c_str(), "", "ext4", "ro", WSLC_MOUNT::KernelModules);
294 +
295 + // Configure GPU mounts if enabled
296 + MountGpuLibraries("/usr/lib/wsl/lib", "/usr/lib/wsl/drivers");
297 +
298 + // Configure cold discard hint size for page reporting.
299 + // This sets the minimum order of pages that will be reported as free to the hypervisor.
300 + {
301 + const auto windowsVersion = wsl::windows::common::helpers::GetWindowsVersion();
302 + int pageReportingOrder = (windowsVersion.BuildNumber >= wsl::windows::common::helpers::WindowsBuildNumbers::Germanium) ? 5 : 9; // 128k or 2MB
303 + auto cmdStr = std::format("echo {} > /sys/module/page_reporting/parameters/page_reporting_order", pageReportingOrder);
304 + std::vector<const char*> args{"/bin/sh", "-c", cmdStr.c_str()};
305 +
306 + WSLCProcessOptions options{};
307 + options.CommandLine = {.Values = args.data(), .Count = static_cast<ULONG>(args.size())};
308 + CreateLinuxProcessImpl("/bin/sh", options, {}, nullptr, [](const auto&) {});
309 + }
310 +
311 + // Configure networking. This must happen after all filesystems are mounted since /gns needs to access /sys.
312 + ConfigureNetworking();
313 +}
314 +
315 +WSLCVirtualMachine::~WSLCVirtualMachine()
316 +{
317 + WSL_LOG("WSLCTerminateVmStart");
318 +
319 + m_vmTerminatingEvent.SetEvent();
320 +
321 + m_initChannel.Close();
322 +
323 + // Terminate the VM.
324 + m_vm.reset();
325 +
326 + if (m_processExitThread.joinable())
327 + {
328 + m_processExitThread.join();
329 + }
330 +
331 + if (m_crashDumpThread.joinable())
332 + {
333 + m_crashDumpThread.join();
334 + }
335 +
336 + // Clear the state of all remaining processes now that the VM has exited.
337 + for (auto& e : m_trackedProcesses)
338 + {
339 + if (auto locked = e.lock())
340 + {
341 + locked->OnVmTerminated();
342 + }
343 + }
344 +}
345 +
346 +void WSLCVirtualMachine::ConfigureNetworking()
347 +{
348 + if (m_networkingMode == WSLCNetworkingModeNone)
349 + {
350 + return;
351 + }
352 +
353 + // Launch /gns with auto-allocated file descriptors for the GNS channel (and DNS channel if enabled).
354 + std::vector<WSLCProcessFd> fds;
355 + fds.emplace_back(WSLCProcessFd{.Fd = -1, .Type = WSLCFdType::WSLCFdTypeDefault});
356 +
357 + bool enableDnsTunneling = FeatureEnabled(WslcFeatureFlagsDnsTunneling);
358 + if (enableDnsTunneling)
359 + {
360 + fds.emplace_back(WSLCProcessFd{.Fd = -1, .Type = WSLCFdType::WSLCFdTypeDefault});
361 + }
362 +
363 + // Because the file descriptor numbers aren't known in advance, the command line needs to be generated after the
364 + // file descriptors are allocated.
365 + std::vector<const char*> cmd{"/gns", LX_INIT_GNS_SOCKET_ARG};
366 + std::string gnsSocketFdArg;
367 + std::string dnsSocketFdArg;
368 + int gnsChannelFd = -1;
369 + int dnsChannelFd = -1;
370 +
371 + WSLCProcessOptions options{};
372 + auto prepareCommandLine = [&](const auto& sockets) {
373 + gnsChannelFd = sockets[0].Fd;
374 + gnsSocketFdArg = std::to_string(gnsChannelFd);
375 + cmd.push_back(gnsSocketFdArg.c_str());
376 +
377 + if (enableDnsTunneling)
378 + {
379 + dnsChannelFd = sockets[1].Fd;
380 + dnsSocketFdArg = std::to_string(dnsChannelFd);
381 + cmd.push_back(LX_INIT_GNS_DNS_SOCKET_ARG);
382 + cmd.push_back(dnsSocketFdArg.c_str());
383 + cmd.push_back(LX_INIT_GNS_DNS_TUNNELING_IP);
384 + cmd.push_back(LX_INIT_DNS_TUNNELING_IP_ADDRESS);
385 + }
386 +
387 + options.CommandLine = {.Values = cmd.data(), .Count = static_cast<ULONG>(cmd.size())};
388 + };
389 +
390 + auto process = CreateLinuxProcessImpl("/init", options, fds, nullptr, prepareCommandLine);
391 +
392 + // Call back to the service to configure the networking engine.
393 + auto gnsHandle = process->GetStdHandle(gnsChannelFd);
394 +
395 + wil::unique_handle dnsHandle;
396 + HANDLE dnsSocketHandle = nullptr;
397 + if (enableDnsTunneling)
398 + {
399 + dnsHandle = process->GetStdHandle(dnsChannelFd);
400 + dnsSocketHandle = dnsHandle.get();
401 + }
402 +
403 + THROW_IF_FAILED(m_vm->ConfigureNetworking(gnsHandle.get(), enableDnsTunneling ? &dnsSocketHandle : nullptr));
404 +
405 + // Launch port relay for port forwarding
406 + LaunchPortRelay();
407 +}
408 +
409 +bool WSLCVirtualMachine::FeatureEnabled(WSLCFeatureFlags Value) const
410 +{
411 + return static_cast<ULONG>(m_featureFlags) & static_cast<ULONG>(Value);
412 +}
413 +
414 +void WSLCVirtualMachine::WatchForExitedProcesses(wsl::shared::SocketChannel& Channel)
415 +try
416 +{
417 + // TODO: Terminate the VM if this thread exits unexpectedly.
418 + while (true)
419 + {
420 + auto [message, _] = Channel.ReceiveMessageOrClosed<WSLC_PROCESS_EXITED>();
421 + if (message == nullptr)
422 + {
423 + break; // Channel has been closed, exit
424 + }
425 +
426 + WSL_LOG(
427 + "ProcessExited",
428 + TraceLoggingValue(message->Pid, "Pid"),
429 + TraceLoggingValue(message->Code, "Code"),
430 + TraceLoggingValue(message->Signaled, "Signaled"));
431 +
432 + // Signal the exited process, if it's been monitored.
433 + // N.B. Lock weak_ptr under lock, then call OnExited outside it to avoid
434 + // deadlock with the destructor's m_lock -> m_trackedProcessesLock ordering.
435 + std::shared_ptr<VMProcessControl> exited;
436 + {
437 + std::lock_guard lock{m_trackedProcessesLock};
438 +
439 + for (auto& e : m_trackedProcesses)
440 + {
441 + auto locked = e.lock();
442 + if (locked && locked->GetPid() == message->Pid)
443 + {
444 + exited = std::move(locked);
445 + break;
446 + }
447 + }
448 + }
449 +
450 + if (exited)
451 + {
452 + try
453 + {
454 + exited->OnExited(message->Signaled ? 128 + message->Code : message->Code);
455 + }
456 + CATCH_LOG();
457 + }
458 + }
459 +}
460 +CATCH_LOG();
461 +
462 +std::pair<ULONG, std::string> WSLCVirtualMachine::AttachDisk(_In_ PCWSTR Path, _In_ BOOL ReadOnly)
463 +{
464 + std::lock_guard lock{m_lock};
465 +
466 + ULONG Lun{};
467 + std::string Device;
468 +
469 + // Delegate to IWSLCVirtualMachine for the privileged HCS operation
470 + THROW_IF_FAILED(m_vm->AttachDisk(Path, ReadOnly, &Lun));
471 +
472 + // Detach on failure so the service-side state stays consistent.
473 + auto detachOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(m_vm->DetachDisk(Lun)); });
474 +
475 + // Query the guest for the device path
476 + Device = GetVhdDevicePath(Lun);
477 +
478 + WSL_LOG(
479 + "WSLCAttachDisk",
480 + TraceLoggingValue(Path, "Path"),
481 + TraceLoggingValue(ReadOnly, "ReadOnly"),
482 + TraceLoggingValue(Device.c_str(), "Device"),
483 + TraceLoggingValue(Lun, "Lun"));
484 +
485 + m_attachedDisks.emplace(Lun, AttachedDisk{Path, Device});
486 +
487 + detachOnFailure.release();
488 +
489 + return {Lun, Device};
490 +}
491 +
492 +void WSLCVirtualMachine::Ext4Format(const std::string& Device)
493 +{
494 + constexpr auto mkfsPath = "/usr/sbin/mkfs.ext4";
495 + ServiceProcessLauncher launcher(mkfsPath, {mkfsPath, Device});
496 + auto result = launcher.Launch(*this).WaitAndCaptureOutput();
497 +
498 + THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
499 +}
500 +
501 +void WSLCVirtualMachine::Unmount(_In_ const char* Path)
502 +{
503 + auto [pid, _, subChannel] = Fork(WSLC_FORK::Thread);
504 +
505 + wsl::shared::MessageWriter<WSLC_UNMOUNT> message;
506 + message.WriteString(Path);
507 +
508 + const auto& response = subChannel.Transaction<WSLC_UNMOUNT>(message.Span());
509 +
510 + // TODO: Return errno to caller
511 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), response.Result == EINVAL);
512 + THROW_HR_IF(E_FAIL, response.Result != 0);
513 +}
514 +
515 +void WSLCVirtualMachine::DetachDisk(_In_ ULONG Lun)
516 +{
517 + std::lock_guard lock{m_lock};
518 +
519 + // Find the disk
520 + auto it = m_attachedDisks.find(Lun);
521 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_attachedDisks.end());
522 +
523 + // Detach it from the guest
524 + WSLC_DETACH message;
525 + message.Lun = Lun;
526 + const auto& response = m_initChannel.Transaction(message);
527 +
528 + // TODO: Return errno to caller
529 + THROW_HR_IF(E_FAIL, response.Result != 0);
530 +
531 + // Remove it from the VM
532 + THROW_IF_FAILED(m_vm->DetachDisk(Lun));
533 +
534 + m_attachedDisks.erase(it);
535 +}
536 +
537 +std::tuple<int32_t, int32_t, wsl::shared::SocketChannel> WSLCVirtualMachine::Fork(enum WSLC_FORK::ForkType Type)
538 +{
539 + std::lock_guard lock{m_lock};
540 + return Fork(m_initChannel, Type);
541 +}
542 +
543 +std::tuple<int32_t, int32_t, wsl::shared::SocketChannel> WSLCVirtualMachine::Fork(
544 + wsl::shared::SocketChannel& Channel, enum WSLC_FORK::ForkType Type, ULONG TtyRows, ULONG TtyColumns)
545 +{
546 + uint32_t port{};
547 + int32_t pid{};
548 + int32_t ptyMaster{};
549 + {
550 + WSLC_FORK message;
551 + message.ForkType = Type;
552 + message.TtyColumns = static_cast<uint16_t>(TtyColumns);
553 + message.TtyRows = static_cast<uint16_t>(TtyRows);
554 + const auto& response = Channel.Transaction(message);
555 + port = response.Port;
556 + pid = response.Pid;
557 + ptyMaster = response.PtyMasterFd;
558 + }
559 +
560 + THROW_HR_IF_MSG(E_FAIL, pid <= 0, "fork() returned %i", pid);
561 +
562 + auto socket = wsl::windows::common::hvsocket::Connect(m_vmId, port, m_vmTerminatingEvent.get(), m_bootTimeoutMs);
563 +
564 + return std::make_tuple(pid, ptyMaster, wsl::shared::SocketChannel{std::move(socket), std::to_string(pid), m_vmTerminatingEvent.get()});
565 +}
566 +
567 +WSLCVirtualMachine::ConnectedSocket WSLCVirtualMachine::ConnectSocket(wsl::shared::SocketChannel& Channel, int32_t Fd)
568 +{
569 + WSLC_ACCEPT message{};
570 + message.Fd = Fd;
571 +
572 + auto transaction = Channel.StartTransaction();
573 + transaction.Send(message);
574 + const auto& response = transaction.Receive<WSLC_ACCEPT::TResponse>();
575 +
576 + ConnectedSocket socket;
577 + socket.Socket = wsl::windows::common::hvsocket::Connect(m_vmId, response.Result);
578 +
579 + // If the FD was unspecified, read the Linux file descriptor from the guest.
580 + if (Fd == -1)
581 + {
582 + socket.Fd = transaction.Receive<RESULT_MESSAGE<int32_t>>().Result;
583 + }
584 + else
585 + {
586 + socket.Fd = Fd;
587 + }
588 +
589 + return socket;
590 +}
591 +
592 +std::string WSLCVirtualMachine::GetVhdDevicePath(ULONG Lun)
593 +{
594 + WSLC_GET_DISK message{};
595 + message.Header.MessageSize = sizeof(message);
596 + message.Header.MessageType = WSLC_GET_DISK::Type;
597 + message.ScsiLun = Lun;
598 + const auto& response = m_initChannel.Transaction(message);
599 + THROW_HR_IF_MSG(E_FAIL, response.Result != 0, "Failed to get disk path, init returned: %lu", response.Result);
600 +
601 + return response.Buffer;
602 +}
603 +
604 +Microsoft::WRL::ComPtr<WSLCProcess> WSLCVirtualMachine::CreateLinuxProcess(
605 + _In_ LPCSTR Executable, _In_ const WSLCProcessOptions& Options, int* Errno, const TPrepareCommandLine& PrepareCommandLine)
606 +{
607 + // Check if this is a tty or not
608 + std::vector<WSLCProcessFd> fds;
609 + if (WI_IsFlagSet(Options.Flags, WSLCProcessFlagsTty))
610 + {
611 + fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDTty, .Type = WSLCFdType::WSLCFdTypeTty});
612 + fds.emplace_back(WSLCProcessFd{.Fd = 0, .Type = WSLCFdType::WSLCFdTypeTtyControl});
613 + }
614 + else
615 + {
616 + if (WI_IsFlagSet(Options.Flags, WSLCProcessFlagsStdin))
617 + {
618 + fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDStdin, .Type = WSLCFdType::WSLCFdTypeDefault});
619 + }
620 +
621 + fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDStdout, .Type = WSLCFdType::WSLCFdTypeDefault});
622 + fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDStderr, .Type = WSLCFdType::WSLCFdTypeDefault});
623 + }
624 +
625 + return CreateLinuxProcessImpl(Executable, Options, fds, Errno, PrepareCommandLine);
626 +}
627 +
628 +Microsoft::WRL::ComPtr<WSLCProcess> WSLCVirtualMachine::CreateLinuxProcessImpl(
629 + LPCSTR Executable, const WSLCProcessOptions& Options, const std::vector<WSLCProcessFd>& Fds, int* Errno, const TPrepareCommandLine& PrepareCommandLine)
630 +{
631 + // N.B This check is there to prevent processes from being started before the VM is done initializing.
632 + // to avoid potential deadlocks, since the processExitThread is required to signal the process exit events.
633 + // std::thread::joinable() is const, so this can be called without acquiring the lock.
634 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_processExitThread.joinable());
635 +
636 + THROW_WIN32_IF_MSG(
637 + ERROR_NOT_SUPPORTED, Options.User != nullptr, "Custom users are not supported for root namespace processes");
638 +
639 + auto setErrno = [Errno](int Error) {
640 + if (Errno != nullptr)
641 + {
642 + *Errno = Error;
643 + }
644 + };
645 +
646 + // Check if this is a tty or not
647 + const WSLCProcessFd* tty = nullptr;
648 + auto [pid, _, childChannel] = Fork(WSLC_FORK::Process);
649 +
650 + std::vector<WSLCVirtualMachine::ConnectedSocket> sockets;
651 + ConnectedSocket ttyControlhandle;
652 + for (const auto& e : Fds)
653 + {
654 +
655 + if (e.Type == WSLCFdTypeTtyControl)
656 + {
657 + THROW_HR_IF_MSG(E_INVALIDARG, ttyControlhandle.Fd != -1, "Multiple terminal control fds specified");
658 +
659 + ttyControlhandle = ConnectSocket(childChannel, e.Fd);
660 + }
661 + else
662 + {
663 + if (e.Type == WSLCFdTypeTty)
664 + {
665 + THROW_HR_IF_MSG(E_INVALIDARG, tty != nullptr, "Multiple terminal fds specified");
666 + tty = &e;
667 + }
668 +
669 + sockets.emplace_back(ConnectSocket(childChannel, e.Fd));
670 + }
671 + }
672 +
673 + PrepareCommandLine(sockets);
674 +
675 + wsl::shared::MessageWriter<WSLC_EXEC> Message;
676 +
677 + Message.WriteString(Message->ExecutableIndex, Executable);
678 + Message.WriteString(Message->CurrentDirectoryIndex, Options.CurrentDirectory ? Options.CurrentDirectory : "/");
679 + Message.WriteStringArray(Message->CommandLineIndex, Options.CommandLine.Values, Options.CommandLine.Count);
680 + Message.WriteStringArray(Message->EnvironmentIndex, Options.Environment.Values, Options.Environment.Count);
681 +
682 + // N.B. The process control needs to be registered before the actual exec message is sent. Otherwise, if the process exits quickly, we might receive the exit notification before registering it.
683 + std::shared_ptr<VMProcessControl> control;
684 + auto registerProcess = [&](int processPid) {
685 + control = std::make_shared<VMProcessControl>(*this, processPid, std::move(ttyControlhandle.Socket));
686 + {
687 + std::lock_guard lock{m_trackedProcessesLock};
688 + m_trackedProcesses.emplace_back(control);
689 + }
690 + };
691 +
692 + // If this is an interactive tty, we need a relay process
693 + if (tty != nullptr)
694 + {
695 + auto [grandChildPid, ptyMaster, grandChildChannel] = Fork(childChannel, WSLC_FORK::Pty, Options.TtyRows, Options.TtyColumns);
696 + WSLC_TTY_RELAY relayMessage{};
697 + relayMessage.TtyMaster = ptyMaster;
698 + relayMessage.Socket = tty->Fd;
699 + relayMessage.TtyControl = ttyControlhandle.Fd; // N.B. Fd is set to -1 if unset.
700 + {
701 + auto relayTransaction = childChannel.StartTransaction();
702 + relayTransaction.Send(relayMessage);
703 + }
704 +
705 + auto result = ExpectClosedChannelOrError(childChannel);
706 + if (result != 0)
707 + {
708 + setErrno(result);
709 + THROW_HR_MSG(E_FAIL, "errno: %i", result);
710 + }
711 +
712 + registerProcess(grandChildPid);
713 +
714 + {
715 + auto execTransaction = grandChildChannel.StartTransaction();
716 + execTransaction.Send<WSLC_EXEC>(Message.Span());
717 + auto [execResponse, execSpan] = execTransaction.ReceiveOrClosed<RESULT_MESSAGE<int32_t>>();
718 + result = execResponse != nullptr ? execResponse->Result : 0;
719 + }
720 + if (result != 0)
721 + {
722 + setErrno(result);
723 + THROW_HR_MSG(E_FAIL, "errno: %i", result);
724 + }
725 +
726 + pid = grandChildPid;
727 + }
728 + else
729 + {
730 + registerProcess(pid);
731 +
732 + auto execTransaction = childChannel.StartTransaction();
733 + execTransaction.Send<WSLC_EXEC>(Message.Span());
734 + auto [execResponse, execSpan] = execTransaction.ReceiveOrClosed<RESULT_MESSAGE<int32_t>>();
735 + auto result = execResponse != nullptr ? execResponse->Result : 0;
736 + if (result != 0)
737 + {
738 + setErrno(result);
739 + THROW_HR_MSG(E_FAIL, "errno: %i", result);
740 + }
741 + }
742 +
743 + std::map<ULONG, TypedHandle> stdHandles;
744 + for (auto& [fd, handle] : sockets)
745 + {
746 + stdHandles.emplace(fd, TypedHandle{wil::unique_handle{reinterpret_cast<HANDLE>(handle.release())}, WSLCHandleTypeSocket});
747 + }
748 +
749 + auto io = std::make_unique<VMProcessIO>(std::move(stdHandles));
750 +
751 + auto process = wil::MakeOrThrow<WSLCProcess>(std::move(control), std::move(io), Options.Flags);
752 +
753 + setErrno(0);
754 +
755 + return process;
756 +}
757 +
758 +void WSLCVirtualMachine::Mount(LPCSTR Source, LPCSTR Target, LPCSTR Type, LPCSTR Options, ULONG Flags)
759 +{
760 + std::lock_guard lock{m_lock};
761 +
762 + Mount(m_initChannel, Source, Target, Type, Options, Flags);
763 +}
764 +
765 +void WSLCVirtualMachine::Mount(shared::SocketChannel& Channel, LPCSTR Source, LPCSTR Target, LPCSTR Type, LPCSTR Options, ULONG Flags)
766 +{
767 + static_assert(WSLCMountFlagsNone == WSLC_MOUNT::None);
768 + static_assert(WSLCMountFlagsReadOnly == WSLC_MOUNT::ReadOnly);
769 + static_assert(WSLCMountFlagsChroot == WSLC_MOUNT::Chroot);
770 + static_assert(WSLCMountFlagsWriteableOverlayFs == WSLC_MOUNT::OverlayFs);
771 +
772 + wsl::shared::MessageWriter<WSLC_MOUNT> message;
773 +
774 + auto optionalAdd = [&](auto value, unsigned int& index) {
775 + if (value != nullptr)
776 + {
777 + message.WriteString(index, value);
778 + }
779 + };
780 +
781 + optionalAdd(Source, message->SourceIndex);
782 + optionalAdd(Target, message->DestinationIndex);
783 + optionalAdd(Type, message->TypeIndex);
784 + optionalAdd(Options, message->OptionsIndex);
785 + message->Flags = Flags;
786 +
787 + const auto& response = Channel.Transaction<WSLC_MOUNT>(message.Span());
788 +
789 + WSL_LOG(
790 + "WSLCMount",
791 + TraceLoggingValue(Source == nullptr ? "<null>" : Source, "Source"),
792 + TraceLoggingValue(Target == nullptr ? "<null>" : Target, "Target"),
793 + TraceLoggingValue(Type == nullptr ? "<null>" : Type, "Type"),
794 + TraceLoggingValue(Options == nullptr ? "<null>" : Options, "Options"),
795 + TraceLoggingValue(Flags, "Flags"),
796 + TraceLoggingValue(response.Result, "Result"));
797 +
798 + THROW_HR_IF(E_FAIL, response.Result != 0);
799 +}
800 +
801 +int32_t WSLCVirtualMachine::ExpectClosedChannelOrError(wsl::shared::SocketChannel& Channel)
802 +{
803 + auto [response, span] = Channel.ReceiveMessageOrClosed<RESULT_MESSAGE<int32_t>>();
804 + if (response != nullptr)
805 + {
806 + return response->Result;
807 + }
808 + else
809 + {
810 + return 0;
811 + }
812 +}
813 +
814 +void WSLCVirtualMachine::Signal(_In_ LONG Pid, _In_ int Signal)
815 +{
816 + std::lock_guard lock(m_lock);
817 +
818 + WSLC_SIGNAL message;
819 + message.Pid = Pid;
820 + message.Signal = Signal;
821 + const auto& response = m_initChannel.Transaction(message);
822 +
823 + THROW_HR_IF(E_FAIL, response.Result != 0);
824 +}
825 +
826 +void WSLCVirtualMachine::LaunchPortRelay()
827 +{
828 + WI_ASSERT(!m_portRelayChannelRead);
829 +
830 + auto [_, __, channel] = Fork(WSLC_FORK::ForkType::Process);
831 +
832 + std::lock_guard lock(m_portRelaylock);
833 + auto relayPort = channel.Transaction<WSLC_PORT_RELAY>();
834 +
835 + wil::unique_handle readPipe;
836 + wil::unique_handle writePipe;
837 + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &m_portRelayChannelWrite, nullptr, 0));
838 + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&m_portRelayChannelRead, &writePipe, nullptr, 0));
839 +
840 + // TODO: move the port relay infra into this process. Create a thread, pass handle ownership to thread, refactor and remove
841 + // wslrelaymode. wsl::windows::wslrelay::localhost::RunWSLCPortRelay(
842 + // readPipe.release(), writePipe.release(), m_vmId, relayPort.Result, m_vmTerminatingEvent.get());
843 +
844 + wsl::windows::common::helpers::SetHandleInheritable(readPipe.get());
845 + wsl::windows::common::helpers::SetHandleInheritable(writePipe.get());
846 + wsl::windows::common::helpers::SetHandleInheritable(m_vmTerminatingEvent.get());
847 +
848 + auto path = wsl::windows::common::wslutil::GetBasePath() / L"wslrelay.exe";
849 +
850 + auto cmd = std::format(
851 + L"\"{}\" {} {} {} {} {} {} {} {}",
852 + path,
853 + wslrelay::mode_option,
854 + static_cast<int>(wslrelay::RelayMode::WSLCPortRelay),
855 + wslrelay::exit_event_option,
856 + HandleToUlong(m_vmTerminatingEvent.get()),
857 + wslrelay::port_option,
858 + relayPort.Result,
859 + wslrelay::vm_id_option,
860 + m_vmId);
861 +
862 + WSL_LOG("LaunchWslRelay", TraceLoggingValue(cmd.c_str(), "cmd"));
863 +
864 + wsl::windows::common::SubProcess process{nullptr, cmd.c_str()};
865 + process.SetStdHandles(readPipe.get(), writePipe.get(), nullptr);
866 + process.Start();
867 +
868 + readPipe.release();
869 + writePipe.release();
870 +}
871 +
872 +void WSLCVirtualMachine::MapRelayPort(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort, _In_ bool Remove)
873 +{
874 + std::lock_guard lock(m_portRelaylock);
875 +
876 + THROW_HR_IF(E_ILLEGAL_STATE_CHANGE, !m_portRelayChannelWrite);
877 +
878 + WSLC_MAP_PORT message;
879 + message.WindowsPort = WindowsPort;
880 + message.LinuxPort = LinuxPort;
881 + message.AddressFamily = Family;
882 + message.Stop = Remove;
883 +
884 + DWORD bytesTransfered{};
885 + THROW_IF_WIN32_BOOL_FALSE(WriteFile(m_portRelayChannelWrite.get(), &message, sizeof(message), &bytesTransfered, nullptr));
886 + THROW_HR_IF_MSG(E_UNEXPECTED, bytesTransfered != sizeof(message), "%u bytes transfered", bytesTransfered);
887 +
888 + HRESULT result = E_UNEXPECTED;
889 + THROW_IF_WIN32_BOOL_FALSE(ReadFile(m_portRelayChannelRead.get(), &result, sizeof(result), &bytesTransfered, nullptr));
890 +
891 + THROW_HR_IF(E_UNEXPECTED, bytesTransfered != sizeof(result));
892 + THROW_IF_FAILED_MSG(result, "Failed to map port: WindowsPort=%d, LinuxPort=%d, Family=%d, Remove=%d", WindowsPort, LinuxPort, Family, Remove);
893 +}
894 +
895 +void WSLCVirtualMachine::MapPort(VMPortMapping& Mapping)
896 +{
897 + THROW_HR_IF_MSG(E_INVALIDARG, !Mapping.VmPort, "Can't map a VM port without an allocated port");
898 +
899 + if (m_networkingMode == WSLCNetworkingModeNone)
900 + {
901 + THROW_HR_MSG(E_ILLEGAL_STATE_CHANGE, "Port mapping is not supported with the current networking mode");
902 + }
903 + else if (m_networkingMode == WSLCNetworkingModeNAT)
904 + {
905 + THROW_HR_IF_MSG(
906 + HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
907 + !Mapping.IsLocalhost() || Mapping.Protocol != IPPROTO_TCP,
908 + "Unsupported port mapping for NAT mode: %hs, protocol: %i",
909 + Mapping.BindingAddressString().c_str(),
910 + Mapping.Protocol);
911 +
912 + MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), false);
913 + }
914 + else if (m_networkingMode == WSLCNetworkingModeVirtioProxy)
915 + {
916 + // TODO: Switch to using the native virtionet relay.
917 + THROW_HR_IF_MSG(
918 + HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
919 + !Mapping.IsLocalhost() || Mapping.Protocol != IPPROTO_TCP,
920 + "Unsupported port mapping for virtionet mode: %hs, protocol: %i",
921 + Mapping.BindingAddressString().c_str(),
922 + Mapping.Protocol);
923 +
924 + MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), false);
925 + }
926 + else
927 + {
928 + THROW_HR_MSG(E_UNEXPECTED, "Unexpected networking mode: %i", m_networkingMode);
929 + }
930 +
931 + Mapping.Attach(*this);
932 +}
933 +
934 +void WSLCVirtualMachine::UnmapPort(VMPortMapping& Mapping)
935 +{
936 + THROW_HR_IF_MSG(E_INVALIDARG, !Mapping.VmPort, "Can't unmap a VM port without an allocated port");
937 +
938 + if (m_networkingMode == WSLCNetworkingModeNone)
939 + {
940 + THROW_HR_MSG(E_ILLEGAL_STATE_CHANGE, "Port mapping is not supported with the current networking mode");
941 + }
942 + else if (m_networkingMode == WSLCNetworkingModeNAT)
943 + {
944 + MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), true);
945 + }
946 + else if (m_networkingMode == WSLCNetworkingModeVirtioProxy)
947 + {
948 + // TODO: Switch to using the native virtionet relay.
949 + MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), true);
950 + }
951 + else
952 + {
953 + THROW_HR_MSG(E_UNEXPECTED, "Unexpected networking mode: %i", m_networkingMode);
954 + }
955 +
956 + Mapping.Detach();
957 +}
958 +
959 +HRESULT WSLCVirtualMachine::MountWindowsFolder(_In_ LPCWSTR WindowsPath, _In_ LPCSTR LinuxPath, _In_ BOOL ReadOnly)
960 +{
961 + return MountWindowsFolderImpl(WindowsPath, LinuxPath, ReadOnly ? WSLCMountFlagsReadOnly : WSLCMountFlagsNone);
962 +}
963 +
964 +HRESULT WSLCVirtualMachine::MountWindowsFolderImpl(_In_ LPCWSTR WindowsPath, _In_ LPCSTR LinuxPath, _In_ WSLCMountFlags Flags)
965 +try
966 +{
967 + std::filesystem::path path(WindowsPath);
968 + THROW_HR_IF_MSG(E_INVALIDARG, !path.is_absolute(), "Path is not absolute: '%ls'", WindowsPath);
969 + THROW_HR_IF_MSG(
970 + HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), !std::filesystem::is_directory(path), "Path is not a directory: '%ls'", WindowsPath);
971 +
972 + const bool readOnly = WI_IsFlagSet(Flags, WSLCMountFlagsReadOnly);
973 + auto normalizedPath = std::filesystem::weakly_canonical(path).wstring();
974 + GUID shareGuid{};
975 + bool reusingShare = false;
976 +
977 + {
978 + std::lock_guard lock(m_lock);
979 +
980 + // Verify that this folder isn't already mounted.
981 + auto it = m_mountedWindowsFolders.find(LinuxPath);
982 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), it != m_mountedWindowsFolders.end());
983 +
984 + // In VirtioFs mode, try to reuse an existing share for the same Windows path and access mode.
985 + if (FeatureEnabled(WslcFeatureFlagsVirtioFs))
986 + {
987 + auto shareIt = m_virtioFsShares.find({normalizedPath, readOnly});
988 + if (shareIt != m_virtioFsShares.end())
989 + {
990 + shareGuid = shareIt->second;
991 + reusingShare = true;
992 + }
993 + }
994 +
995 + if (!reusingShare)
996 + {
997 + // Delegate to IWSLCVirtualMachine for the privileged share creation
998 + THROW_IF_FAILED(m_vm->AddShare(WindowsPath, readOnly, &shareGuid));
999 +
1000 + if (FeatureEnabled(WslcFeatureFlagsVirtioFs))
1001 + {
1002 + m_virtioFsShares[{normalizedPath, readOnly}] = shareGuid;
1003 + }
1004 + }
1005 +
1006 + m_mountedWindowsFolders.emplace(LinuxPath, shareGuid);
1007 + }
1008 +
1009 + auto deleteOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1010 + std::lock_guard lock(m_lock);
1011 + auto mountIt = m_mountedWindowsFolders.find(LinuxPath);
1012 + if (WI_VERIFY(mountIt != m_mountedWindowsFolders.end()))
1013 + {
1014 + m_mountedWindowsFolders.erase(mountIt);
1015 + if (!FeatureEnabled(WslcFeatureFlagsVirtioFs))
1016 + {
1017 + m_virtioFsShares.erase({normalizedPath, readOnly});
1018 + LOG_IF_FAILED(m_vm->RemoveShare(shareGuid));
1019 + }
1020 + }
1021 + });
1022 +
1023 + // Create the guest mount
1024 + auto shareName = shared::string::GuidToString<char>(shareGuid, shared::string::None);
1025 + if (!FeatureEnabled(WslcFeatureFlagsVirtioFs))
1026 + {
1027 + auto [_, __, channel] = Fork(WSLC_FORK::Process);
1028 +
1029 + WSLC_CONNECT message;
1030 + message.HostPort = LX_INIT_UTILITY_VM_PLAN9_PORT;
1031 +
1032 + auto fd = channel.Transaction(message).Result;
1033 + THROW_HR_IF_MSG(E_FAIL, fd < 0, "WSLC_CONNECT failed with %i", fd);
1034 +
1035 + auto mountOptions = std::format(
1036 + "{},msize={},trans=fd,rfdno={},wfdno={},aname={},cache=mmap", readOnly ? "ro" : "rw", LX_INIT_UTILITY_VM_PLAN9_BUFFER_SIZE, fd, fd, shareName);
1037 +
1038 + Mount(channel, shareName.c_str(), LinuxPath, "9p", mountOptions.c_str(), Flags);
1039 + }
1040 + else
1041 + {
1042 + std::string options = readOnly ? "ro" : "rw";
1043 + Mount(m_initChannel, shareName.c_str(), LinuxPath, "virtiofs", options.c_str(), Flags);
1044 + }
1045 +
1046 + deleteOnFailure.release();
1047 +
1048 + return S_OK;
1049 +}
1050 +CATCH_RETURN();
1051 +
1052 +HRESULT WSLCVirtualMachine::UnmountWindowsFolder(_In_ LPCSTR LinuxPath)
1053 +try
1054 +{
1055 + std::lock_guard lock(m_lock);
1056 +
1057 + // Verify that this folder is mounted.
1058 + auto it = m_mountedWindowsFolders.find(LinuxPath);
1059 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_mountedWindowsFolders.end());
1060 +
1061 + // Unmount the folder from the guest.
1062 + auto result = wil::ResultFromException([&]() { Unmount(LinuxPath); });
1063 + THROW_HR_IF(result, FAILED(result) && result != HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
1064 +
1065 + auto shareId = it->second;
1066 +
1067 + // Keep the share mounted in virtiofs mode to avoid accumulating devices, which can cause a hang when reached.
1068 + // TODO: Actually remove the device once this is supported by the device host.
1069 + if (!FeatureEnabled(WslcFeatureFlagsVirtioFs))
1070 + {
1071 + // Delegate to IWSLCVirtualMachine for the privileged share removal
1072 + THROW_IF_FAILED(m_vm->RemoveShare(shareId));
1073 + }
1074 +
1075 + m_mountedWindowsFolders.erase(it);
1076 +
1077 + return S_OK;
1078 +}
1079 +CATCH_RETURN();
1080 +
1081 +void WSLCVirtualMachine::MountGpuLibraries(_In_ LPCSTR LibrariesMountPoint, _In_ LPCSTR DriversMountpoint)
1082 +{
1083 + if (!FeatureEnabled(WslcFeatureFlagsGPU))
1084 + {
1085 + return;
1086 + }
1087 +
1088 + auto windowsPath = wil::GetWindowsDirectoryW<std::wstring>();
1089 +
1090 + // Mount drivers.
1091 + THROW_IF_FAILED(MountWindowsFolderImpl(
1092 + std::format(L"{}\\System32\\DriverStore\\FileRepository", windowsPath).c_str(), DriversMountpoint, WSLCMountFlagsReadOnly));
1093 +
1094 + // Mount the inbox libraries.
1095 + auto inboxLibPath = std::format(L"{}\\System32\\lxss\\lib", windowsPath);
1096 + std::optional<std::string> inboxLibMountPoint;
1097 + if (std::filesystem::is_directory(inboxLibPath))
1098 + {
1099 + inboxLibMountPoint = std::format("{}/inbox", LibrariesMountPoint);
1100 + THROW_IF_FAILED(MountWindowsFolderImpl(inboxLibPath.c_str(), inboxLibMountPoint->c_str(), WSLCMountFlagsReadOnly));
1101 + }
1102 +
1103 + // Mount the packaged libraries.
1104 +#ifdef WSL_GPU_LIB_PATH
1105 +
1106 + auto packagedLibPath = std::filesystem::path(TEXT(WSL_GPU_LIB_PATH));
1107 +
1108 +#else
1109 +
1110 + auto packagedLibPath = wslutil::GetBasePath() / L"lib";
1111 +
1112 +#endif
1113 +
1114 + auto packagedLibMountPoint = std::format("{}/packaged", LibrariesMountPoint);
1115 + THROW_IF_FAILED(MountWindowsFolderImpl(packagedLibPath.c_str(), packagedLibMountPoint.c_str(), WSLCMountFlagsReadOnly));
1116 +
1117 + // Mount an overlay containing both inbox and packaged libraries (the packaged mount takes precedence).
1118 + std::string options = "lowerdir=" + packagedLibMountPoint;
1119 + if (inboxLibMountPoint.has_value())
1120 + {
1121 + options += ":" + inboxLibMountPoint.value();
1122 + }
1123 +
1124 + Mount(m_initChannel, "none", LibrariesMountPoint, "overlay", options.c_str(), 0);
1125 +}
1126 +
1127 +void WSLCVirtualMachine::OnProcessReleased(int Pid)
1128 +{
1129 + std::lock_guard lock{m_trackedProcessesLock};
1130 +
1131 + std::erase_if(m_trackedProcesses, [Pid](const auto& e) {
1132 + auto locked = e.lock();
1133 + return !locked || locked->GetPid() == Pid;
1134 + });
1135 +}
1136 +
1137 +std::shared_ptr<VmPortAllocation> WSLCVirtualMachine::TryAllocatePort(uint16_t Port, int Family, int Protocol)
1138 +{
1139 + std::lock_guard lock{m_lock};
1140 +
1141 + WSL_LOG("AllocatePort", TraceLoggingValue(Port, "Port"));
1142 +
1143 + auto [_, inserted] = m_allocatedPorts.insert(Port);
1144 +
1145 + if (inserted)
1146 + {
1147 + return std::make_shared<VmPortAllocation>(Port, Family, Protocol, *this);
1148 + }
1149 + else
1150 + {
1151 + return {};
1152 + }
1153 +}
1154 +
1155 +std::shared_ptr<VmPortAllocation> WSLCVirtualMachine::AllocatePort(int Family, int Protocol)
1156 +{
1157 + std::lock_guard lock{m_lock};
1158 +
1159 + for (auto i = CONTAINER_PORT_RANGE.first; i <= CONTAINER_PORT_RANGE.second; i++)
1160 + {
1161 + if (!m_allocatedPorts.contains(i))
1162 + {
1163 + WI_VERIFY(m_allocatedPorts.insert(i).second);
1164 + return std::make_shared<VmPortAllocation>(i, Family, Protocol, *this);
1165 + }
1166 + }
1167 +
1168 + // Fail if we couldn't find a port.
1169 + THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NO_SYSTEM_RESOURCES), "Failed to allocate port");
1170 +}
1171 +
1172 +void WSLCVirtualMachine::ReleasePort(VmPortAllocation& Port)
1173 +{
1174 + std::lock_guard lock{m_lock};
1175 +
1176 + WSL_LOG("ReleasePort", TraceLoggingValue(Port.Port(), "Port"));
1177 +
1178 + LOG_HR_IF(E_UNEXPECTED, m_allocatedPorts.erase(Port.Port()) != 1);
1179 +}
1180 +
1181 +wil::unique_socket WSLCVirtualMachine::ConnectUnixSocket(const char* Path)
1182 +{
1183 + auto [_, __, channel] = Fork(WSLC_FORK::Thread);
1184 +
1185 + shared::MessageWriter<WSLC_UNIX_CONNECT> message;
1186 + message.WriteString(message->PathOffset, Path);
1187 +
1188 + auto result = channel.Transaction<WSLC_UNIX_CONNECT>(message.Span());
1189 +
1190 + THROW_HR_IF_MSG(E_FAIL, result.Result < 0, "Failed to connect to unix socket: '%hs', %i", Path, result.Result);
1191 +
1192 + return channel.Release();
1193 +}
1194 +
1195 +void WSLCVirtualMachine::CollectCrashDumps(wil::unique_socket&& listenSocket)
1196 +{
1197 + // No impersonation needed - the session process already runs as the user.
1198 + wslutil::SetThreadDescription(L"CrashDumpCollection");
1199 +
1200 + const auto crashDumpFolder = filesystem::GetTempFolderPath(GetCurrentProcessToken()) / L"wslc-crashes";
1201 +
1202 + while (!m_vmTerminatingEvent.is_signaled())
1203 + {
1204 + try
1205 + {
1206 + auto socket = hvsocket::CancellableAccept(listenSocket.get(), INFINITE, m_vmTerminatingEvent.get());
1207 + if (!socket)
1208 + {
1209 + // VM is exiting.
1210 + break;
1211 + }
1212 +
1213 + constexpr DWORD timeout = 30 * 1000;
1214 + THROW_LAST_ERROR_IF(setsockopt(socket->get(), SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)) == SOCKET_ERROR);
1215 +
1216 + auto channel = wsl::shared::SocketChannel{std::move(socket.value()), "crash_dump", m_vmTerminatingEvent.get()};
1217 +
1218 + auto transaction = channel.ReceiveTransaction();
1219 + gsl::span<gsl::byte> responseSpan;
1220 + const auto& message = transaction.Receive<LX_PROCESS_CRASH>(&responseSpan);
1221 +
1222 + const auto bufferSize = responseSpan.size_bytes() - offsetof(LX_PROCESS_CRASH, Buffer);
1223 + const std::string process(message.Buffer, strnlen(message.Buffer, bufferSize));
1224 +
1225 + constexpr auto dumpExtension = ".dmp";
1226 + constexpr auto dumpPrefix = "wsl-crash";
1227 +
1228 + auto filename = std::format("{}-{}-{}-{}-{}{}", dumpPrefix, message.Timestamp, message.Pid, process, message.Signal, dumpExtension);
1229 +
1230 + std::replace_if(
1231 + filename.begin(),
1232 + filename.end(),
1233 + [](char e) { return !std::isalnum(static_cast<unsigned char>(e)) && e != '.' && e != '-'; },
1234 + '_');
1235 +
1236 + auto fullPath = crashDumpFolder / filename;
1237 +
1238 + WSL_LOG(
1239 + "WSLCLinuxCrash",
1240 + TraceLoggingValue(fullPath.c_str(), "FullPath"),
1241 + TraceLoggingValue(message.Pid, "Pid"),
1242 + TraceLoggingValue(message.Signal, "Signal"),
1243 + TraceLoggingValue(process.c_str(), "process"));
1244 +
1245 + filesystem::EnsureDirectory(crashDumpFolder.c_str());
1246 +
1247 + // Only delete files that:
1248 + // - have the temporary flag set
1249 + // - start with 'wsl-crash'
1250 + // - end in .dmp
1251 + //
1252 + // This logic is here to prevent accidental user file deletion
1253 + auto pred = [&dumpExtension, &dumpPrefix](const auto& e) {
1254 + return WI_IsFlagSet(GetFileAttributes(e.path().c_str()), FILE_ATTRIBUTE_TEMPORARY) && e.path().has_extension() &&
1255 + e.path().extension() == dumpExtension && e.path().has_filename() &&
1256 + e.path().filename().string().find(dumpPrefix) == 0;
1257 + };
1258 +
1259 + wslutil::EnforceFileLimit(crashDumpFolder.c_str(), 10, pred);
1260 +
1261 + wil::unique_hfile file{CreateFileW(fullPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, nullptr)};
1262 + THROW_LAST_ERROR_IF(!file);
1263 +
1264 + transaction.SendResultMessage<std::int32_t>(0);
1265 + relay::InterruptableRelay(reinterpret_cast<HANDLE>(channel.Socket()), file.get(), nullptr);
1266 + }
1267 + CATCH_LOG()
1268 + }
1269 +}
src/windows/wslcsession/WSLCVirtualMachine.h new
+238
@@ -0,0 +1,238 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCVirtualMachine.h
8 +
9 +Abstract:
10 +
11 + WSLCVirtualMachine manages the client-side lifecycle of a WSLC virtual machine.
12 +
13 + The VM is created via IWSLCVirtualMachine (running in the SYSTEM service), and this class
14 + connects to the existing VM for unprivileged operations. Privileged operations
15 + like AttachDisk and AddShare are delegated back to IWSLCVirtualMachine.
16 +
17 +--*/
18 +#pragma once
19 +#include "wslc.h"
20 +#include "hcs.hpp"
21 +#include "WSLCProcess.h"
22 +#include "WSLCContainerMetadata.h"
23 +#include <thread>
24 +#include <filesystem>
25 +
26 +namespace wsl::windows::service::wslc {
27 +
28 +enum WSLCMountFlags
29 +{
30 + WSLCMountFlagsNone = 0,
31 + WSLCMountFlagsReadOnly = 1,
32 + WSLCMountFlagsChroot = 2,
33 + WSLCMountFlagsWriteableOverlayFs = 4,
34 +};
35 +
36 +enum WSLCFdType
37 +{
38 + WSLCFdTypeDefault = 0,
39 + WSLCFdTypeTty = 1,
40 + WSLCFdTypeTtyControl = 2,
41 +};
42 +
43 +struct WSLCProcessFd
44 +{
45 + LONG Fd{};
46 + WSLCFdType Type{};
47 +};
48 +
49 +class WSLCVirtualMachine;
50 +
51 +struct VmPortAllocation
52 +{
53 + NON_COPYABLE(VmPortAllocation);
54 +
55 + VmPortAllocation(uint16_t port, int Family, int Protocol, WSLCVirtualMachine& vm);
56 + VmPortAllocation(VmPortAllocation&& Other);
57 + ~VmPortAllocation();
58 +
59 + VmPortAllocation& operator=(VmPortAllocation&& Other);
60 +
61 + void Reset();
62 + void Release();
63 + uint16_t Port() const;
64 + int Family() const;
65 + int Protocol() const;
66 +
67 +private:
68 + uint16_t m_port{};
69 + int m_family{};
70 + int m_protocol{};
71 + WSLCVirtualMachine* m_vm{};
72 +};
73 +
74 +struct VMPortMapping
75 +{
76 + NON_COPYABLE(VMPortMapping);
77 +
78 + VMPortMapping(int Protocol, int Family, uint16_t Port, const char* Address);
79 + ~VMPortMapping();
80 +
81 + VMPortMapping(VMPortMapping&& Other);
82 + VMPortMapping& operator=(VMPortMapping&& Other);
83 +
84 + void AssignVmPort(const std::shared_ptr<VmPortAllocation>& Port);
85 +
86 + void Unmap();
87 + void Release();
88 + bool IsLocalhost() const;
89 + bool IsIPv6() const;
90 + std::string BindingAddressString() const;
91 + void Attach(WSLCVirtualMachine& Vm);
92 + void Detach();
93 + uint16_t HostPort() const;
94 +
95 + static VMPortMapping LocalhostTcpMapping(int Family, uint16_t WindowsPort);
96 + static VMPortMapping FromWSLCPortMapping(const ::WSLCPortMapping& Mapping);
97 + static VMPortMapping FromContainerMetaData(const wslc::WSLCPortMapping& Mapping);
98 +
99 + int Protocol{};
100 + std::shared_ptr<VmPortAllocation> VmPort;
101 + SOCKADDR_INET BindAddress{};
102 +
103 +private:
104 + static SOCKADDR_INET ParseBindingAddress(int Family, uint16_t Port, const char* Address);
105 +
106 + WSLCVirtualMachine* Vm{};
107 +};
108 +
109 +class WSLCVirtualMachine
110 +{
111 +public:
112 + struct ConnectedSocket
113 + {
114 + int Fd = -1;
115 + wil::unique_socket Socket;
116 + };
117 +
118 + using TPrepareCommandLine = std::function<void(const std::vector<ConnectedSocket>&)>;
119 +
120 + WSLCVirtualMachine(_In_ IWSLCVirtualMachine* Vm, _In_ const WSLCSessionInitSettings* Settings);
121 + ~WSLCVirtualMachine();
122 +
123 + void Initialize();
124 +
125 + void MapPort(VMPortMapping& Mapping);
126 + void UnmapPort(VMPortMapping& Mapping);
127 + void Unmount(_In_ const char* Path);
128 +
129 + HRESULT MountWindowsFolder(_In_ LPCWSTR WindowsPath, _In_ LPCSTR LinuxPath, _In_ BOOL ReadOnly);
130 + HRESULT UnmountWindowsFolder(_In_ LPCSTR LinuxPath);
131 + void Signal(_In_ LONG Pid, _In_ int Signal);
132 +
133 + void OnProcessReleased(int Pid);
134 +
135 + std::shared_ptr<VmPortAllocation> TryAllocatePort(uint16_t Port, int Family, int Protocol);
136 + std::shared_ptr<VmPortAllocation> AllocatePort(int Family, int Protocol);
137 + void ReleasePort(VmPortAllocation& Port);
138 +
139 + Microsoft::WRL::ComPtr<WSLCProcess> CreateLinuxProcess(
140 + _In_ LPCSTR Executable,
141 + _In_ const WSLCProcessOptions& Options,
142 + int* Errno = nullptr,
143 + const TPrepareCommandLine& PrepareCommandLine = [](const auto&) {});
144 +
145 + std::pair<ULONG, std::string> AttachDisk(_In_ PCWSTR Path, _In_ BOOL ReadOnly);
146 + void DetachDisk(_In_ ULONG Lun);
147 + void Ext4Format(_In_ const std::string& Device);
148 + void Mount(_In_ LPCSTR Source, _In_ LPCSTR Target, _In_ LPCSTR Type, _In_ LPCSTR Options, _In_ ULONG Flags);
149 +
150 + wil::unique_socket ConnectUnixSocket(_In_ const char* Path);
151 + std::tuple<int32_t, int32_t, wsl::shared::SocketChannel> Fork(enum WSLC_FORK::ForkType Type);
152 +
153 + // Returns an event that is signaled when the VM is being terminated.
154 + // Use this to cancel pending operations.
155 + HANDLE TerminatingEvent() const
156 + {
157 + return m_vmTerminatingEvent.get();
158 + }
159 +
160 + GUID VmId() const
161 + {
162 + return m_vmId;
163 + }
164 +
165 +private:
166 + void MapRelayPort(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort, _In_ bool Remove);
167 +
168 + // Initial setup during Connect()
169 + void ConfigureNetworking();
170 +
171 + static void Mount(wsl::shared::SocketChannel& Channel, LPCSTR Source, _In_ LPCSTR Target, _In_ LPCSTR Type, _In_ LPCSTR Options, _In_ ULONG Flags);
172 + void MountGpuLibraries(_In_ LPCSTR LibrariesMountPoint, _In_ LPCSTR DriversMountpoint);
173 +
174 + Microsoft::WRL::ComPtr<WSLCProcess> CreateLinuxProcessImpl(
175 + _In_ LPCSTR Executable,
176 + _In_ const WSLCProcessOptions& Options,
177 + _In_ const std::vector<WSLCProcessFd>& Fds = {},
178 + int* Errno = nullptr,
179 + const TPrepareCommandLine& PrepareCommandLine = [](const auto&) {});
180 +
181 + bool FeatureEnabled(WSLCFeatureFlags Flag) const;
182 +
183 + std::tuple<int32_t, int32_t, wsl::shared::SocketChannel> Fork(
184 + wsl::shared::SocketChannel& Channel, enum WSLC_FORK::ForkType Type, ULONG TtyRows = 0, ULONG TtyColumns = 0);
185 + int32_t ExpectClosedChannelOrError(wsl::shared::SocketChannel& Channel);
186 +
187 + ConnectedSocket ConnectSocket(wsl::shared::SocketChannel& Channel, int32_t Fd);
188 + std::string GetVhdDevicePath(ULONG Lun);
189 + void LaunchPortRelay();
190 +
191 + HRESULT MountWindowsFolderImpl(_In_ LPCWSTR WindowsPath, _In_ LPCSTR LinuxPath, _In_ WSLCMountFlags Flags = WSLCMountFlagsNone);
192 +
193 + void WatchForExitedProcesses(wsl::shared::SocketChannel& Channel);
194 +
195 + void CollectCrashDumps(wil::unique_socket&& listenSocket);
196 +
197 + struct AttachedDisk
198 + {
199 + std::filesystem::path Path;
200 + std::string Device;
201 + };
202 +
203 + // IWSLCVirtualMachine for privileged operations on this VM
204 + wil::com_ptr<IWSLCVirtualMachine> m_vm;
205 +
206 + WSLCFeatureFlags m_featureFlags{};
207 + WSLCNetworkingMode m_networkingMode{};
208 + ULONG m_bootTimeoutMs{};
209 +
210 + std::string m_rootVhdType;
211 +
212 + std::thread m_processExitThread;
213 + std::thread m_crashDumpThread;
214 +
215 + std::set<uint16_t> m_allocatedPorts;
216 +
217 + GUID m_vmId{};
218 +
219 + std::mutex m_trackedProcessesLock;
220 + std::vector<std::weak_ptr<VMProcessControl>> m_trackedProcesses;
221 +
222 + wil::unique_event m_vmTerminatingEvent{wil::EventOptions::ManualReset};
223 +
224 + wsl::shared::SocketChannel m_initChannel;
225 + wil::unique_handle m_portRelayChannelRead;
226 + wil::unique_handle m_portRelayChannelWrite;
227 +
228 + std::map<ULONG, AttachedDisk> m_attachedDisks;
229 + std::map<std::string, GUID> m_mountedWindowsFolders;
230 +
231 + // VirtioFs share cache: maps (normalized WindowsPath, readOnly) to share GUID.
232 + // Shares are kept alive after unmount for reuse on subsequent mounts of the same folder.
233 + std::map<std::pair<std::wstring, bool>, GUID> m_virtioFsShares;
234 +
235 + std::recursive_mutex m_lock;
236 + std::mutex m_portRelaylock;
237 +};
238 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCVolumeMetadata.h new
+39
@@ -0,0 +1,39 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCVolumeMetadata.h
8 +
9 +Abstract:
10 +
11 + JSON schema for WSLC volume metadata stored in Docker volume labels.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "JsonUtils.h"
18 +
19 +namespace wsl::windows::service::wslc {
20 +
21 +// Label key used to store WSLC volume metadata in Docker volume labels.
22 +constexpr auto WSLCVolumeMetadataLabel = "com.microsoft.wsl.volume.metadata";
23 +
24 +// Volume driver name for VHD-backed volumes.
25 +constexpr auto WSLCVhdVolumeDriver = "vhd";
26 +
27 +// Volume driver name for guest-backed volumes (passthrough to docker's built-in "local" driver).
28 +constexpr auto WSLCGuestVolumeDriver = "guest";
29 +
30 +struct WSLCVolumeMetadata
31 +{
32 + std::string Driver;
33 + std::map<std::string, std::string> DriverOpts;
34 + std::map<std::string, std::string> Properties;
35 +
36 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(WSLCVolumeMetadata, Driver, DriverOpts, Properties);
37 +};
38 +
39 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/application.manifest new
+8
@@ -0,0 +1,8 @@
1 +<?xml version="1.0" encoding="utf-8" standalone="yes"?>
2 +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
3 +<application xmlns="urn:schemas-microsoft-com:asm.v3">
4 + <windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
5 + <ws2:longPathAware>true</ws2:longPathAware>
6 + </windowsSettings>
7 +</application>
8 +</assembly>
src/windows/wslcsession/main.cpp new
+99
@@ -0,0 +1,99 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + main.cpp
8 +
9 +Abstract:
10 +
11 + Entry point for wslcsession.exe - the per-user COM server for WSLC sessions.
12 +
13 + This runs under the user's identity and hosts WSLCSessionFactory COM objects.
14 + The SYSTEM service creates sessions via IWSLCSessionFactory::CreateSession,
15 + which returns both the session and a service reference for lifetime tracking.
16 +
17 +--*/
18 +
19 +#include "precomp.h"
20 +#include "WSLCSessionFactory.h"
21 +
22 +using namespace wsl::windows::common;
23 +using namespace wsl::windows::common::wslutil;
24 +
25 +namespace {
26 +
27 +// Event used to signal that the COM server should exit.
28 +wil::unique_event g_exitEvent{wil::EventOptions::ManualReset};
29 +
30 +class WSLCSessionFactoryClassFactory : public winrt::implements<WSLCSessionFactoryClassFactory, IClassFactory>
31 +{
32 +public:
33 + STDMETHODIMP CreateInstance(_In_ IUnknown* outer, REFIID iid, _COM_Outptr_ void** result) noexcept override
34 + try
35 + {
36 + *result = nullptr;
37 + THROW_HR_IF(CLASS_E_NOAGGREGATION, outer != nullptr);
38 +
39 + auto factory = Microsoft::WRL::Make<wsl::windows::service::wslc::WSLCSessionFactory>();
40 + factory->SetDestructionCallback([]() { g_exitEvent.SetEvent(); });
41 + return factory->QueryInterface(iid, result);
42 + }
43 + CATCH_RETURN()
44 +
45 + STDMETHODIMP LockServer(BOOL) noexcept override
46 + {
47 + return S_OK;
48 + }
49 +};
50 +
51 +} // namespace
52 +
53 +int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
54 +try
55 +{
56 + ConfigureCrt();
57 +
58 + // Enable contextualized errors
59 + wsl::windows::common::EnableContextualizedErrors(true);
60 +
61 + // Initialize telemetry
62 + WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild);
63 + auto cleanup = wil::scope_exit([] { WslTraceLoggingUninitialize(); });
64 +
65 + // Don't kill the process on unknown C++ exceptions
66 + wil::g_fResultFailFastUnknownExceptions = false;
67 +
68 + wsl::windows::common::security::ApplyProcessMitigationPolicies();
69 +
70 + // Initialize Winsock
71 + WSADATA data;
72 + THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &data));
73 +
74 + WSL_LOG("Per-user session server starting", TraceLoggingLevel(WINEVENT_LEVEL_INFO));
75 +
76 + // Initialize COM
77 + auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
78 + wsl::windows::common::wslutil::CoInitializeSecurity();
79 +
80 + // Register the class factory (single-use: one factory per process)
81 + auto factory = winrt::make<WSLCSessionFactoryClassFactory>();
82 + wil::unique_com_class_object_cookie cookie;
83 + THROW_IF_FAILED(::CoRegisterClassObject(
84 + __uuidof(wsl::windows::service::wslc::WSLCSessionFactory), factory.get(), CLSCTX_LOCAL_SERVER, REGCLS_SINGLEUSE, &cookie));
85 +
86 + WSL_LOG("Per-user session server registered, waiting for activations", TraceLoggingLevel(WINEVENT_LEVEL_INFO));
87 +
88 + // Wait until all objects have been released
89 + g_exitEvent.wait();
90 +
91 + WSL_LOG("Per-user session server exiting", TraceLoggingLevel(WINEVENT_LEVEL_INFO));
92 +
93 + return 0;
94 +}
95 +catch (...)
96 +{
97 + LOG_CAUGHT_EXCEPTION();
98 + return 1;
99 +}
src/windows/wslcsession/main.rc new
+28
@@ -0,0 +1,28 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + main.rc
8 +
9 +Abstract:
10 +
11 + This file contains resources for wslcsession.
12 +
13 +--*/
14 +
15 +#include <windows.h>
16 +#include "resource.h"
17 +#include "wslversioninfo.h"
18 +
19 +#define VER_INTERNALNAME_STR "wslcsession.exe"
20 +#define VER_ORIGINALFILENAME_STR "wslcsession.exe"
21 +
22 +#define VER_FILETYPE VFT_APP
23 +#define VER_FILESUBTYPE VFT2_UNKNOWN
24 +#define VER_FILEDESCRIPTION_STR "Windows Subsystem for Linux Containers User Session"
25 +ID_ICON ICON PRELOAD DISCARDABLE "..\..\..\Images\wsl.ico"
26 +
27 +
28 +#include <common.ver>
src/windows/wslcsession/resource.h new
+15
@@ -0,0 +1,15 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + resource.h
8 +
9 +Abstract:
10 +
11 + This file contains resource declarations for wslcsession.exe
12 +
13 +--*/
14 +
15 +#define ID_ICON 1
src/windows/wslrelay/localhost.cpp
+315
@@ -329,3 +329,318 @@ try
329 }
330 }
331 CATCH_LOG()
332 +
333 +struct PortRelay
334 +{
335 + wil::unique_socket ListenSocket;
336 + uint32_t LinuxPort;
337 + uint32_t RelayPort;
338 + wil::unique_event AcceptEvent{wil::EventOptions::None};
339 + OVERLAPPED Overlapped{};
340 + bool Pending = false;
341 + wil::unique_socket PendingSocket;
342 + int Family;
343 + CHAR AcceptBuffer[2 * sizeof(SOCKADDR_STORAGE)]{};
344 +
345 + PortRelay(wil::unique_socket&& ListenSocket, uint32_t LinuxPort, uint32_t RelayPort, int Family) :
346 + ListenSocket(std::move(ListenSocket)), LinuxPort(LinuxPort), RelayPort(RelayPort), Family(Family)
347 + {
348 + Overlapped.hEvent = AcceptEvent.get();
349 + }
350 +
351 + ~PortRelay()
352 + {
353 + if (Pending) // Cancel pending accept(), if any.
354 + {
355 + DWORD bytesProcessed;
356 + DWORD flagsReturned;
357 + CancelIoEx(reinterpret_cast<HANDLE>(ListenSocket.get()), &Overlapped);
358 + WSAGetOverlappedResult(ListenSocket.get(), &Overlapped, &bytesProcessed, TRUE, &flagsReturned);
359 + }
360 + }
361 +
362 + void LaunchRelay(const GUID& VmId)
363 + {
364 + WI_VERIFY(PendingSocket);
365 +
366 + std::thread thread{
367 + [WindowsSocket = std::move(PendingSocket), LinuxPort = LinuxPort, RelayPort = RelayPort, Family = Family, VmId = VmId]() {
368 + try
369 + {
370 + WSL_LOG(
371 + "StartPortRelay",
372 + TraceLoggingValue(LinuxPort, "LinuxPort"),
373 + TraceLoggingValue(WindowsSocket.get(), "Socket"),
374 + TraceLoggingValue(Family, "Family"));
375 +
376 + RunRelay(WindowsSocket.get(), VmId, LinuxPort, RelayPort, Family);
377 + }
378 + CATCH_LOG();
379 +
380 + WSL_LOG(
381 + "StopPortRelay",
382 + TraceLoggingValue(LinuxPort, "LinuxPort"),
383 + TraceLoggingValue(WindowsSocket.get(), "Socket"),
384 + TraceLoggingValue(Family, "Family"));
385 + }};
386 +
387 + thread.detach();
388 + }
389 +
390 + static void RunRelay(SOCKET WindowsSocket, const GUID& VmId, uint32_t LinuxPort, uint32_t RelayPort, uint32_t Family)
391 + {
392 + wsl::shared::SocketChannel channel(wsl::windows::common::hvsocket::Connect(VmId, RelayPort), "SocketRelay");
393 +
394 + WI_VERIFY(Family == AF_INET || Family == AF_INET6);
395 + LX_INIT_START_SOCKET_RELAY message{};
396 + message.Port = LinuxPort;
397 + message.Family = Family == AF_INET ? LX_AF_INET : LX_AF_INET6;
398 + message.BufferSize = LOCALHOST_RELAY_BUFFER_SIZE;
399 + channel.SendMessage(message);
400 +
401 + wsl::windows::common::relay::SocketRelay(WindowsSocket, channel.Socket(), message.BufferSize);
402 + }
403 +
404 + void CompleteAccept()
405 + {
406 + Pending = false;
407 +
408 + DWORD bytes{};
409 + DWORD flags{};
410 + if (!WSAGetOverlappedResult(ListenSocket.get(), &Overlapped, &bytes, false, &flags))
411 + {
412 + THROW_WIN32(WSAGetLastError());
413 + }
414 + }
415 +
416 + bool ScheduleAccept()
417 + {
418 + WI_VERIFY(!Pending);
419 +
420 + PendingSocket.reset(WSASocket(Family, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_OVERLAPPED));
421 + memset(AcceptBuffer, 0, sizeof(AcceptBuffer));
422 + DWORD BytesReturned{};
423 + if (!AcceptEx(ListenSocket.get(), PendingSocket.get(), AcceptBuffer, 0, sizeof(SOCKADDR_STORAGE), sizeof(SOCKADDR_STORAGE), &BytesReturned, &Overlapped))
424 + {
425 + const int error = WSAGetLastError();
426 + THROW_HR_IF(HRESULT_FROM_WIN32(error), error != WSA_IO_PENDING);
427 +
428 + Pending = true;
429 + return false;
430 + }
431 +
432 + return true;
433 + }
434 +};
435 +
436 +std::shared_ptr<PortRelay> CreatePortListener(uint16_t WindowsPort, uint16_t LinuxPort, uint32_t RelayPort, int Family)
437 +{
438 + wil::unique_socket ListenSocket(WSASocket(Family, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_OVERLAPPED));
439 +
440 + THROW_LAST_ERROR_IF(!ListenSocket);
441 +
442 + constexpr BOOLEAN On = true;
443 + THROW_LAST_ERROR_IF(setsockopt(ListenSocket.get(), SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&On), sizeof(On)) == SOCKET_ERROR);
444 +
445 + sockaddr* Address{};
446 + sockaddr_in InetAddress{};
447 + sockaddr_in6 Inet6Address{};
448 + DWORD AddressSize{};
449 + if (Family == AF_INET)
450 + {
451 + InetAddress.sin_family = AF_INET;
452 + InetAddress.sin_port = htons(WindowsPort);
453 + InetAddress.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
454 + Address = reinterpret_cast<sockaddr*>(&InetAddress);
455 + AddressSize = sizeof(InetAddress);
456 + }
457 + else
458 + {
459 + Inet6Address.sin6_family = AF_INET6;
460 + Inet6Address.sin6_port = htons(WindowsPort);
461 + Inet6Address.sin6_addr = IN6ADDR_LOOPBACK_INIT;
462 + Address = reinterpret_cast<sockaddr*>(&Inet6Address);
463 + AddressSize = sizeof(Inet6Address);
464 + }
465 +
466 + THROW_LAST_ERROR_IF(bind(ListenSocket.get(), Address, AddressSize) == SOCKET_ERROR);
467 + THROW_LAST_ERROR_IF(listen(ListenSocket.get(), -1) == SOCKET_ERROR);
468 +
469 + return std::make_shared<PortRelay>(std::move(ListenSocket), LinuxPort, RelayPort, Family);
470 +}
471 +
472 +void AcceptThread(std::vector<std::shared_ptr<PortRelay>>& ports, const GUID& VmId, HANDLE ExitEvent)
473 +{
474 + while (true)
475 + {
476 + // First make sure that all the accept() are scheduled
477 + std::vector<HANDLE> events{ExitEvent};
478 + for (auto& e : ports)
479 + {
480 + if (!e->Pending)
481 + {
482 + while (e->ScheduleAccept())
483 + {
484 + e->LaunchRelay(VmId); // Start the relay if accept completes immediately.
485 + }
486 + }
487 +
488 + events.push_back(e->AcceptEvent.get());
489 + }
490 +
491 + // WaitForMultipleObjects supports at most MAXIMUM_WAIT_OBJECTS (64) handles.
492 + auto result = WaitForMultipleObjects(static_cast<DWORD>(events.size()), events.data(), false, INFINITE);
493 + THROW_LAST_ERROR_IF(result == WAIT_FAILED);
494 +
495 + if (result == 0) // If the exit event is signaled, leave the loop
496 + {
497 + break;
498 + }
499 +
500 + // Otherwise complete the accept and start a relay
501 + try
502 + {
503 + ports[result - 1]->CompleteAccept();
504 + ports[result - 1]->LaunchRelay(VmId);
505 + }
506 + CATCH_LOG();
507 + }
508 +}
509 +
510 +std::optional<WSLC_MAP_PORT> ReceiveServiceMessage()
511 +{
512 + WSLC_MAP_PORT message{};
513 +
514 + DWORD bytesRead{};
515 + if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &message, sizeof(message), &bytesRead, nullptr))
516 + {
517 + LOG_LAST_ERROR();
518 + return {};
519 + }
520 + else if (bytesRead == 0)
521 + {
522 + return {};
523 + }
524 +
525 + WI_ASSERT(message.Header.MessageSize == sizeof(message));
526 + WI_ASSERT(message.Header.MessageType == LxMessageWSLCMapPort);
527 + return message;
528 +}
529 +
530 +void wsl::windows::wslrelay::localhost::RunWSLCPortRelay(const GUID& VmId, uint32_t RelayPort, HANDLE ExitEvent)
531 +{
532 + std::map<std::tuple<uint16_t, uint32_t>, std::shared_ptr<PortRelay>> ports;
533 +
534 + std::thread acceptThread;
535 + wil::unique_event acceptThreadEvent{wil::EventOptions::ManualReset};
536 +
537 + auto stopAcceptThread = [&]() {
538 + if (acceptThread.joinable())
539 + {
540 + acceptThreadEvent.SetEvent();
541 + acceptThread.join();
542 + acceptThread = {};
543 + acceptThreadEvent.ResetEvent();
544 + }
545 + };
546 +
547 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { stopAcceptThread(); });
548 +
549 + while (true)
550 + {
551 + // Receive a message
552 + auto message = ReceiveServiceMessage();
553 + if (!message.has_value())
554 + {
555 + return;
556 + }
557 +
558 + std::tuple<uint16_t, uint16_t> key{message->WindowsPort, message->AddressFamily};
559 +
560 + HRESULT result = E_UNEXPECTED;
561 + auto sendResponse = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
562 + WSL_LOG(
563 + "PortMapping",
564 + TraceLoggingValue(result, "Result"),
565 + TraceLoggingValue(message->AddressFamily, "Family"),
566 + TraceLoggingValue(message->WindowsPort, "WindowsPort"),
567 + TraceLoggingValue(message->LinuxPort, "LinuxPort"),
568 + TraceLoggingValue(message->Stop, "Remove"));
569 +
570 + THROW_LAST_ERROR_IF(!WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), &result, sizeof(result), nullptr, nullptr));
571 + });
572 +
573 + // Check if the binding is valid.
574 + bool update = false;
575 + auto it = ports.find(key);
576 + if (message->Stop)
577 + {
578 + if (it == ports.end())
579 + {
580 + result = HRESULT_FROM_WIN32(ERROR_NOT_FOUND);
581 + continue;
582 + }
583 + else
584 + {
585 + ports.erase(it);
586 + update = true;
587 + }
588 + }
589 + else
590 + {
591 + if (it != ports.end())
592 + {
593 + result = HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS);
594 + continue;
595 + }
596 + else
597 + {
598 + // WaitForMultipleObjects supports at most MAXIMUM_WAIT_OBJECTS (64) handles.
599 + // Reject the mapping if adding it would exceed the limit (1 handle reserved for the exit event).
600 + constexpr size_t c_maxPorts = MAXIMUM_WAIT_OBJECTS - 1;
601 + if (ports.size() >= c_maxPorts)
602 + {
603 + result = HRESULT_FROM_WIN32(ERROR_TOO_MANY_OPEN_FILES);
604 + continue;
605 + }
606 +
607 + try
608 + {
609 + ports.emplace(key, CreatePortListener(message->WindowsPort, message->LinuxPort, RelayPort, message->AddressFamily));
610 + update = true;
611 + }
612 + catch (...)
613 + {
614 + result = wil::ResultFromCaughtException();
615 + continue;
616 + }
617 + }
618 + }
619 +
620 + // Update the ports list
621 + if (update)
622 + {
623 + stopAcceptThread();
624 + }
625 +
626 + // Start the accept thread, if needed
627 + if (!acceptThread.joinable())
628 + {
629 + std::vector<std::shared_ptr<PortRelay>> relays;
630 + for (auto& e : ports)
631 + {
632 + relays.emplace_back(e.second);
633 + }
634 +
635 + acceptThread = std::thread([&, relays = std::move(relays)]() mutable {
636 + try
637 + {
638 + AcceptThread(relays, VmId, acceptThreadEvent.get());
639 + }
640 + CATCH_LOG();
641 + });
642 + }
643 +
644 + result = S_OK;
645 + }
646 +}
\ No newline at end of file
src/windows/wslrelay/localhost.h
+2
@@ -47,6 +47,8 @@ typedef struct _LX_PORT_LISTENER_CONTEXT
47 namespace wsl::windows::wslrelay::localhost {
48 void RelayWorker(_In_ wsl::shared::SocketChannel& SocketChannel, _In_ const GUID& VmId);
49
50 +void RunWSLCPortRelay(const GUID& VmId, uint32_t RelayPort, HANDLE ExitEvent);
51 +
52 class Relay
53 {
54 public:
src/windows/wslrelay/main.cpp
+6
@@ -101,6 +101,12 @@ try
101 break;
102 }
103
104 + case wslrelay::RelayMode::WSLCPortRelay:
105 + {
106 + wsl::windows::wslrelay::localhost::RunWSLCPortRelay(vmId, port, exitEvent.get());
107 + break;
108 + }
109 +
110 case wslrelay::RelayMode::KdRelay:
111 {
112 THROW_HR_IF(E_INVALIDARG, port == 0);
src/windows/wslsettings/CMakeLists.txt
+3 -33
@@ -1,6 +1,6 @@
1 set(TargetApp wslsettings)
2
3 -project(${TargetApp} LANGUAGES CSharp)
3 +enable_language(CSharp)
4
5 # needed for csharp_set_xaml_cs_properties
6 include(CSharpUtilities)
@@ -184,21 +184,16 @@ csharp_set_xaml_cs_properties(
184 Views/Settings/ShellPage.xaml.cs
185 )
186
187 +configure_csharp_target(${TargetApp})
188 +
189 set_property(
190 SOURCE App.xaml
191 PROPERTY VS_XAML_TYPE
192 "ApplicationDefinition"
193 )
194
193 -# Set the C# language version (defaults to 3.0).
194 -set(
195 - CMAKE_CSharp_FLAGS
196 - "/langversion:latest"
197 -)
198 -
195 target_compile_options(
196 ${TargetApp}
201 - PRIVATE "/debug:full"
197 PRIVATE "/unsafe"
198 )
199
@@ -219,23 +214,6 @@ Microsoft.Xaml.Behaviors.WinUI.Managed_${XAML_BEHAVIORS_VERSION};\
214 WinUIEx_${WINUIEX_VERSION}"
215 )
216
222 -set(
223 - TARGET_PLATFORM_VERSION
224 - "10.0.26100.0"
225 -)
226 -set(
227 - WINDOWS_TARGET_PLATFORM_VERSION
228 - "windows${TARGET_PLATFORM_VERSION}"
229 -)
230 -set(
231 - TARGET_PLATFORM_MIN_VERSION
232 - "10.0.19041.0"
233 -)
234 -set(
235 - WINDOWS_TARGET_PLATFORM_MIN_VERSION
236 - "windows${TARGET_PLATFORM_MIN_VERSION}"
237 -)
238 -
217 set_target_properties(
218 ${TargetApp} PROPERTIES
219 # ----- Dotnet, Windows App SDK and WinUI stuff starts here -----
@@ -253,16 +231,8 @@ set_target_properties(
231 VS_GLOBAL_Platform "${TARGET_PLATFORM}"
232 VS_GLOBAL_Platforms "${TARGET_PLATFORM}"
233 VS_GLOBAL_PlatformTarget "${TARGET_PLATFORM}"
256 - VS_GLOBAL_TargetPlatformVersion "${TARGET_PLATFORM_VERSION}"
257 - VS_GLOBAL_TargetPlatformMinVersion "${TARGET_PLATFORM_MIN_VERSION}"
258 - VS_GLOBAL_WindowsSdkPackageVersion "${WINDOWS_SDK_DOTNET_VERSION}"
234 VS_GLOBAL_ImplicitUsings enable
235 VS_GLOBAL_Nullable enable
261 - VS_GLOBAL_AppendRuntimeIdentifierToOutputPath false
262 - VS_GLOBAL_GenerateAssemblyInfo false
263 - VS_GLOBAL_TargetLatestRuntimePatch false
264 - DOTNET_SDK "Microsoft.NET.Sdk"
265 - DOTNET_TARGET_FRAMEWORK "net8.0-${WINDOWS_TARGET_PLATFORM_VERSION}"
236 )
237
238 configure_file(
test/windows/CMakeLists.txt
+17 -4
@@ -8,7 +8,10 @@ set(SOURCES
8 Common.cpp
9 PluginTests.cpp
10 PolicyTests.cpp
11 - InstallerTests.cpp)
11 + InstallerTests.cpp
12 + WSLCTests.cpp
13 + WslcSdkTests.cpp
14 + WindowsUpdateTests.cpp)
15
16 set(HEADERS
17 Common.h
@@ -19,18 +22,28 @@ add_compile_definitions(INLINE_TEST_METHOD_MARKUP)
22
23 add_library(wsltests SHARED ${SOURCES} ${HEADERS})
24
25 +target_include_directories(wsltests PRIVATE ${CMAKE_SOURCE_DIR}/src/windows/WslcSDK)
26 +target_link_directories(wsltests PRIVATE ${BIN})
27 target_precompile_headers(wsltests REUSE_FROM common)
28 target_link_libraries(wsltests
29 common
30 + wslclib
31 + wslcsdk
32 ${TAEF_LINK_LIBRARIES}
33 ${COMMON_LINK_LIBRARIES}
34 ${MSI_LINK_LIBRARIES}
35 ${HCS_LINK_LIBRARIES}
36 + yaml-cpp
37 ${SERVICE_LINK_LIBRARIES}
38 VirtDisk.lib
39 Wer.lib
40 Dbghelp.lib
33 - sfc.lib)
41 + sfc.lib
42 + Crypt32.lib)
43
35 -add_dependencies(wsltests wslserviceidl)
36 -add_subdirectory(testplugin)
\ No newline at end of file
44 +add_dependencies(wsltests wslserviceidl wslclib wslc wslcsdk)
45 +add_subdirectory(testplugin)
46 +add_subdirectory(wslc)
47 +
48 +# For prettier source tree browsing
49 +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES} ${HEADERS})
test/windows/Common.cpp
+419 -9
@@ -20,6 +20,7 @@ Abstract:
20 #include <tlhelp32.h>
21 #include <werapi.h>
22 #include <Dbghelp.h>
23 +#include <winsafer.h>
24
25 using namespace WEX::Logging;
26 using namespace WEX::Common;
@@ -65,6 +66,9 @@ std::optional<std::wstring> g_dumpToolPath;
66 static bool g_enableWerReport = false;
67 static std::wstring g_pipelineBuildId;
68 std::wstring g_testDistroPath;
69 +std::wstring g_testDataPath;
70 +bool g_fastTestRun = false; // True when test.bat was invoked with -f
71 +static wil::unique_mta_usage_cookie g_mtaCookie;
72
73 std::pair<wil::unique_handle, wil::unique_handle> CreateSubprocessPipe(bool inheritRead, bool inheritWrite, DWORD bufferSize, _In_opt_ SECURITY_ATTRIBUTES* sa)
74 {
@@ -823,7 +827,15 @@ void CreateProcessCrashReport(DWORD Pid, LPCWSTR ImageName, LPCWSTR EventName)
827 void CreateWerReports()
828 {
829 static const std::set<std::wstring, wsl::shared::string::CaseInsensitiveCompare> WslProcesses{
826 - L"wsl.exe", L"wslhost.exe", L"wslrelay.exe", L"wslservice.exe", L"wslg.exe", L"vmcompute.exe", L"vmwp.exe"};
830 + L"wsl.exe",
831 + L"wslhost.exe",
832 + L"wslrelay.exe",
833 + L"wslservice.exe",
834 + L"wslg.exe",
835 + L"vmcompute.exe",
836 + L"vmwp.exe",
837 + L"wslcsession.exe",
838 + L"wslc.exe"};
839
840 auto PrivilegeState = wsl::windows::common::security::AcquirePrivilege(SE_DEBUG_NAME);
841 const std::wstring EventName = L"WslTestHang-" + g_pipelineBuildId;
@@ -1316,12 +1328,26 @@ void StopWslService()
1328 StopService(service.get());
1329 }
1330
1319 -wil::unique_handle GetNonElevatedToken()
1331 +wil::unique_handle GetNonElevatedToken(TOKEN_TYPE Type)
1332 {
1321 - const auto token = wil::open_current_access_token(TOKEN_ALL_ACCESS);
1333 + auto token = wil::open_current_access_token(TOKEN_ALL_ACCESS);
1334 +
1335 + if (Type != TokenPrimary)
1336 + {
1337 + // N.B. Using the Safer API to create a non-elevated primary token break drvfs, so skipping this for primary tokens.
1338 + SAFER_LEVEL_HANDLE saferLevel = nullptr;
1339 + auto closeSaferLevel = wil::scope_exit([&]() { SaferCloseLevel(saferLevel); });
1340 +
1341 + THROW_IF_WIN32_BOOL_FALSE(SaferCreateLevel(SAFER_SCOPEID_MACHINE, SAFER_LEVELID_NORMALUSER, SAFER_LEVEL_OPEN, &saferLevel, nullptr));
1342 +
1343 + wil::unique_handle restrictedToken;
1344 + THROW_IF_WIN32_BOOL_FALSE(SaferComputeTokenFromLevel(saferLevel, token.get(), &restrictedToken, 0, nullptr));
1345 +
1346 + token = std::move(restrictedToken);
1347 + }
1348
1349 wil::unique_handle nonElevatedToken;
1324 - THROW_IF_WIN32_BOOL_FALSE(DuplicateTokenEx(token.get(), TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, TokenPrimary, &nonElevatedToken));
1350 + THROW_IF_WIN32_BOOL_FALSE(DuplicateTokenEx(token.get(), TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, Type, &nonElevatedToken));
1351
1352 wil::unique_sid mediumIntegritySid;
1353 THROW_LAST_ERROR_IF(!ConvertStringSidToSidA("S-1-16-8192", &mediumIntegritySid));
@@ -1363,14 +1389,24 @@ WslConfigChange::~WslConfigChange()
1389 }
1390 }
1391
1392 +std::wstring ReadFileContent(const std::string& Path)
1393 +{
1394 + std::ifstream configRead(Path);
1395 + return std::wstring{std::istreambuf_iterator<char>(configRead), {}};
1396 +}
1397 +
1398 +std::wstring ReadFileContent(const std::wstring& Path)
1399 +{
1400 + std::wifstream configRead(Path);
1401 + return std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
1402 +}
1403 +
1404 // writes global WSL 2 config settings at %userprofile%/.wslconfig
1405 std::wstring LxssWriteWslConfig(const std::wstring& Content)
1406 {
1407 auto path = getenv("userprofile") + std::string("\\.wslconfig");
1408
1371 - std::wifstream configRead(path);
1372 - auto previousContent = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
1373 - configRead.close();
1409 + auto previousContent = ReadFileContent(path);
1410
1411 std::wofstream config(path);
1412 VERIFY_IS_TRUE(config.good());
@@ -1937,6 +1973,10 @@ Return Value:
1973 --*/
1974
1975 {
1976 + wsl::windows::common::wslutil::InitializeWil();
1977 +
1978 + THROW_IF_FAILED(CoIncrementMTAUsage(&g_mtaCookie));
1979 +
1980 // Don't crash for unknown exceptions (makes debugging testpasses harder)
1981 #ifndef _DEBUG
1982 wil::g_fResultFailFastUnknownExceptions = false;
@@ -2035,11 +2075,14 @@ Return Value:
2075
2076 g_testDistroPath = getTestParam(L"DistroPath");
2077
2078 + g_testDataPath = getTestParam(L"TestDataPath");
2079 +
2080 const auto setupScript = getOptionalTestParam(L"SetupScript");
2081 if (!setupScript.has_value())
2082 {
2083 // If no setup script is present, mark test_distro as the default distro here for convenience.
2084 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--set-default " LXSS_DISTRO_NAME_TEST_L), 0L);
2085 + g_fastTestRun = true;
2086
2087 return true;
2088 }
@@ -2092,6 +2135,11 @@ Return Value:
2135 {
2136 LogInfo("Exiting UnitTests module");
2137
2138 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
2139 + WslTraceLoggingUninitialize();
2140 + g_mtaCookie.reset();
2141 + });
2142 +
2143 //
2144 // Release the watchdog timer.
2145 //
@@ -2147,8 +2195,6 @@ Return Value:
2195 wsl::windows::common::registry::WriteString(userKey.get(), nullptr, L"DefaultDistribution", g_originalDefaultDistro.c_str());
2196 }
2197
2150 - WslTraceLoggingUninitialize();
2151 -
2198 return true;
2199 }
2200
@@ -2526,3 +2572,367 @@ void DistroFileChange::Delete()
2572 {
2573 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-u root rm -f '{}'", m_path).c_str()), 0L);
2574 }
2575 +
2576 +std::string ReadToString(SOCKET Handle)
2577 +{
2578 + std::string output;
2579 + DWORD offset = 0;
2580 + while (true) // TODO: timeout
2581 + {
2582 + constexpr auto bufferSize = 512;
2583 +
2584 + output.resize(output.size() + bufferSize);
2585 + int bytesRead = 0;
2586 +
2587 + if ((bytesRead = recv(Handle, &output[offset], bufferSize, 0)) < 0)
2588 + {
2589 + LogError("recv failed with %lu", GetLastError());
2590 + VERIFY_FAIL();
2591 + }
2592 +
2593 + if (bytesRead == 0)
2594 + {
2595 + output.resize(offset);
2596 + break;
2597 + }
2598 +
2599 + output.resize(offset + bytesRead);
2600 + offset += bytesRead;
2601 + }
2602 +
2603 + return output;
2604 +}
2605 +
2606 +std::string ReadToString(HANDLE Handle)
2607 +{
2608 + std::string output;
2609 + DWORD offset = 0;
2610 + constexpr DWORD bufferSize = 4096;
2611 +
2612 + while (true)
2613 + {
2614 + output.resize(offset + bufferSize);
2615 + DWORD bytesRead = 0;
2616 + if (!ReadFile(Handle, output.data() + offset, bufferSize, &bytesRead, nullptr))
2617 + {
2618 + VERIFY_ARE_EQUAL(GetLastError(), ERROR_BROKEN_PIPE);
2619 + }
2620 +
2621 + offset += bytesRead;
2622 + output.resize(offset);
2623 + if (bytesRead == 0)
2624 + {
2625 + break;
2626 + }
2627 + }
2628 +
2629 + return output;
2630 +}
2631 +
2632 +void VerifyPatternMatch(const std::string& Content, const std::string& Pattern)
2633 +{
2634 + if (!PathMatchSpecA(Content.c_str(), Pattern.c_str()))
2635 + {
2636 + std::wstring message = std::format(L"Output: '{}' didn't match pattern: '{}'", Content, Pattern);
2637 + VERIFY_FAIL(message.c_str());
2638 + }
2639 +}
2640 +
2641 +std::string EscapeString(const std::string& Input)
2642 +{
2643 + std::string Output;
2644 +
2645 + for (const auto& e : Input)
2646 + {
2647 + if (e == '\n')
2648 + {
2649 + Output += "\\n";
2650 + }
2651 + else if (e == '\r')
2652 + {
2653 + Output += "\\r";
2654 + }
2655 + else if (e == '\0')
2656 + {
2657 + Output += "\\0";
2658 + }
2659 + else if (e == '\t')
2660 + {
2661 + Output += "\\t";
2662 + }
2663 + else if (e == '\x1b') // ESC character - start of VT sequence
2664 + {
2665 + Output += "\\x1b";
2666 + }
2667 + else
2668 + {
2669 + Output += e;
2670 + }
2671 + }
2672 +
2673 + return Output;
2674 +}
2675 +
2676 +PartialHandleRead::PartialHandleRead(HANDLE Handle) : m_handle(Handle)
2677 +{
2678 + m_thread = std::thread(std::bind(&PartialHandleRead::Run, this));
2679 +}
2680 +
2681 +PartialHandleRead::~PartialHandleRead()
2682 +{
2683 + m_exitEvent.SetEvent();
2684 + if (m_thread.joinable())
2685 + {
2686 + m_thread.join();
2687 + }
2688 +}
2689 +
2690 +std::string PartialHandleRead::ReadBytes(size_t Length)
2691 +{
2692 + wsl::shared::retry::RetryWithTimeout<void>(
2693 + [&]() {
2694 + std::lock_guard lock{m_mutex};
2695 +
2696 + THROW_HR_IF(E_ABORT, m_data.size() < Length);
2697 + },
2698 + std::chrono::milliseconds(100),
2699 + std::chrono::seconds(60));
2700 +
2701 + std::lock_guard lock{m_mutex};
2702 +
2703 + return m_data.substr(0, Length);
2704 +}
2705 +
2706 +std::string PartialHandleRead::ConsumeBytes(size_t Length)
2707 +{
2708 + wsl::shared::retry::RetryWithTimeout<void>(
2709 + [&]() {
2710 + std::lock_guard lock{m_mutex};
2711 +
2712 + THROW_HR_IF(E_ABORT, m_data.size() < Length);
2713 + },
2714 + std::chrono::milliseconds(100),
2715 + std::chrono::seconds(60));
2716 +
2717 + std::lock_guard lock{m_mutex};
2718 + std::string result = m_data.substr(0, Length);
2719 + m_data.erase(0, Length);
2720 + return result;
2721 +}
2722 +
2723 +std::string PartialHandleRead::GetData() const
2724 +{
2725 + std::lock_guard lock{m_mutex};
2726 + return m_data;
2727 +}
2728 +
2729 +void PartialHandleRead::Expect(const std::string& Expected)
2730 +{
2731 + auto content = ReadBytes(Expected.size());
2732 +
2733 + VERIFY_ARE_EQUAL(content, Expected);
2734 +}
2735 +
2736 +void PartialHandleRead::ExpectConsume(const std::string& Expected)
2737 +{
2738 + auto content = ConsumeBytes(Expected.size());
2739 +
2740 + if (content != Expected)
2741 + {
2742 + VERIFY_FAIL(std::format(
2743 + L"Expected: '{}' but got: '{}'",
2744 + wsl::shared::string::MultiByteToWide(EscapeString(Expected)),
2745 + wsl::shared::string::MultiByteToWide(EscapeString(content)))
2746 + .c_str());
2747 + }
2748 +}
2749 +
2750 +void PartialHandleRead::ExpectClosed(DWORD Timeout)
2751 +{
2752 + VERIFY_ARE_EQUAL(WaitForSingleObject(m_thread.native_handle(), Timeout), WAIT_OBJECT_0);
2753 +}
2754 +
2755 +void PartialHandleRead::Run()
2756 +try
2757 +{
2758 + std::vector<gsl::byte> buffer(4096);
2759 +
2760 + while (!m_exitEvent.is_signaled())
2761 + {
2762 + auto bytesRead = wsl::windows::common::relay::InterruptableRead(m_handle, gsl::make_span(buffer), {m_exitEvent.get()});
2763 + if (bytesRead == 0)
2764 + {
2765 + break;
2766 + }
2767 +
2768 + std::lock_guard lock{m_mutex};
2769 + m_data.append(reinterpret_cast<char*>(buffer.data()), bytesRead);
2770 + }
2771 +}
2772 +CATCH_LOG();
2773 +
2774 +class ReadHandleWithTargetValue : public wsl::windows::common::relay::ReadHandle
2775 +{
2776 +public:
2777 + NON_COPYABLE(ReadHandleWithTargetValue);
2778 + NON_MOVABLE(ReadHandleWithTargetValue);
2779 +
2780 + ReadHandleWithTargetValue(wsl::windows::common::relay::HandleWrapper&& MovedHandle, std::string_view targetValue) :
2781 + ReadHandle(std::move(MovedHandle), [this](const auto& buffer) { m_readBuffer.append(buffer.data(), buffer.size()); }),
2782 + m_targetValue(targetValue)
2783 + {
2784 + }
2785 +
2786 + void Schedule() override
2787 + {
2788 + ReadHandle::Schedule();
2789 + CheckIfTargetFound();
2790 + }
2791 +
2792 + void Collect() override
2793 + {
2794 + ReadHandle::Collect();
2795 + CheckIfTargetFound();
2796 + }
2797 +
2798 +private:
2799 + void CheckIfTargetFound()
2800 + {
2801 + using namespace wsl::windows::common::relay;
2802 +
2803 + if (State == IOHandleStatus::Standby || State == IOHandleStatus::Completed)
2804 + {
2805 + bool targetFound = (m_readBuffer.find(m_targetValue) != std::string::npos);
2806 +
2807 + if (State == IOHandleStatus::Standby)
2808 + {
2809 + if (targetFound)
2810 + {
2811 + State = IOHandleStatus::Completed;
2812 + }
2813 + }
2814 + else
2815 + {
2816 + THROW_WIN32_IF(ERROR_NOT_FOUND, !targetFound);
2817 + }
2818 + }
2819 + }
2820 +
2821 + std::string m_readBuffer;
2822 + std::string m_targetValue;
2823 +};
2824 +
2825 +void WaitForOutput(wil::unique_handle handle, std::string_view targetValue, std::chrono::milliseconds timeout)
2826 +{
2827 + wsl::windows::common::relay::MultiHandleWait io;
2828 + io.AddHandle(std::make_unique<ReadHandleWithTargetValue>(std::move(handle), targetValue));
2829 + io.Run(timeout);
2830 +}
2831 +
2832 +std::filesystem::path GetTestImagePath(std::string_view imageName)
2833 +{
2834 + std::filesystem::path result = std::filesystem::path{g_testDataPath};
2835 +
2836 + if (imageName == "debian:latest")
2837 + {
2838 + result /= L"debian-latest.tar";
2839 + }
2840 + else if (imageName == "python:3.12-alpine")
2841 + {
2842 + result /= L"python-3_12-alpine.tar";
2843 + }
2844 + else if (imageName == "alpine:latest")
2845 + {
2846 + result /= L"alpine-latest.tar";
2847 + }
2848 + else if (imageName == "hello-world:latest")
2849 + {
2850 + result /= L"HelloWorldSaved.tar";
2851 + }
2852 + else if (imageName == "wslc-registry:latest")
2853 + {
2854 + result /= L"wslc-registry.tar";
2855 + }
2856 + else
2857 + {
2858 + THROW_HR_MSG(E_INVALIDARG, "Unknown test image: %hs", imageName.data());
2859 + }
2860 +
2861 + return result;
2862 +}
2863 +
2864 +void ExpectHttpResponse(LPCWSTR Url, std::optional<int> expectedCode, bool retry)
2865 +{
2866 + const winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter filter;
2867 + filter.CacheControl().WriteBehavior(winrt::Windows::Web::Http::Filters::HttpCacheWriteBehavior::NoCache);
2868 +
2869 + const winrt::Windows::Web::Http::HttpClient client(filter);
2870 +
2871 + const auto sendRequest = [&]() {
2872 + try
2873 + {
2874 + LogInfo("Sending request to: %ls", Url);
2875 + auto response = client.GetAsync(winrt::Windows::Foundation::Uri(Url)).get();
2876 + auto content = response.Content().ReadAsStringAsync().get();
2877 +
2878 + if (expectedCode.has_value())
2879 + {
2880 + VERIFY_ARE_EQUAL(static_cast<int>(response.StatusCode()), expectedCode.value());
2881 + }
2882 + else
2883 + {
2884 + LogError("Unexpected reply for: %ls", Url);
2885 + VERIFY_FAIL();
2886 + }
2887 + }
2888 + catch (...)
2889 + {
2890 + auto result = wil::ResultFromCaughtException();
2891 +
2892 + if (!expectedCode.has_value())
2893 + {
2894 + // We currently reset the connection if connect() fails inside
2895 + // the VM. Consider failing the Windows connect() instead.
2896 + VERIFY_ARE_EQUAL(result, HRESULT_FROM_WIN32(WININET_E_INVALID_SERVER_RESPONSE));
2897 + return;
2898 + }
2899 +
2900 + // Throw so RetryWithTimeout can decide whether to retry.
2901 + THROW_HR(result);
2902 + }
2903 + };
2904 +
2905 + if (retry)
2906 + {
2907 + wsl::shared::retry::RetryWithTimeout<void>(sendRequest, std::chrono::milliseconds(500), std::chrono::seconds(30), [&]() {
2908 + return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(WININET_E_INVALID_SERVER_RESPONSE);
2909 + });
2910 + }
2911 + else
2912 + {
2913 + sendRequest();
2914 + }
2915 +}
2916 +
2917 +void SetPathAccess(const std::filesystem::path& path, DWORD Permissions, ACCESS_MODE Mode)
2918 +{
2919 + auto [everyoneSid, everyoneSidBuffer] = wsl::windows::common::security::CreateSid(SECURITY_WORLD_SID_AUTHORITY, SECURITY_WORLD_RID);
2920 +
2921 + EXPLICIT_ACCESSW ea{};
2922 + ea.grfAccessPermissions = Permissions;
2923 + ea.grfAccessMode = Mode;
2924 + ea.grfInheritance = NO_INHERITANCE;
2925 + ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
2926 + ea.Trustee.ptstrName = static_cast<LPWSTR>(everyoneSid);
2927 +
2928 + PACL acl = nullptr;
2929 + wil::unique_hlocal descriptor;
2930 + THROW_IF_WIN32_ERROR(
2931 + GetNamedSecurityInfoW(path.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &acl, nullptr, &descriptor));
2932 +
2933 + wsl::windows::common::security::unique_acl newAcl;
2934 + THROW_IF_WIN32_ERROR(SetEntriesInAclW(1, &ea, acl, &newAcl));
2935 +
2936 + THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(
2937 + const_cast<LPWSTR>(path.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, newAcl.get(), nullptr));
2938 +}
test/windows/Common.h
+169 -28
@@ -27,6 +27,8 @@ Abstract:
27 #include "wslutil.h"
28 #include "WslCoreConfig.h"
29
30 +using namespace std::chrono_literals;
31 +
32 //
33 // N.B. This is also defined in 'lxtcommon.h' & 'lxsetup.ps1'. Update those
34 // files too, if the distro name changes here.
@@ -103,20 +105,30 @@ Abstract:
105 return; \
106 }
107
108 +#define WSL_TEST_CLASS_PROPERTIES \
109 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"LxssManager.dll") \
110 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"LxssManagerProxyStub.dll") \
111 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslclient.dll") \
112 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslservice.exe") \
113 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"WslServiceProxyStub.dll") \
114 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslhost.exe") \
115 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslrelay.exe") \
116 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslconfig.exe") \
117 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wsl.exe") \
118 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslg.exe") \
119 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"msrdc.exe") \
120 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"msal.wsl.proxy.exe") \
121 + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslcsession.exe")
122 +
123 #define WSL_TEST_CLASS(_name) \
124 BEGIN_TEST_CLASS(_name) \
108 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"LxssManager.dll") \
109 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"LxssManagerProxyStub.dll") \
110 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslclient.dll") \
111 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslservice.exe") \
112 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"WslServiceProxyStub.dll") \
113 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslhost.exe") \
114 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslrelay.exe") \
115 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslconfig.exe") \
116 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wsl.exe") \
117 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wslg.exe") \
118 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"msrdc.exe") \
119 - TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"msal.wsl.proxy.exe") \
125 + WSL_TEST_CLASS_PROPERTIES \
126 + END_TEST_CLASS()
127 +
128 +#define WSLC_TEST_CLASS(_name) \
129 + BEGIN_TEST_CLASS(_name) \
130 + WSL_TEST_CLASS_PROPERTIES \
131 + TEST_CLASS_PROPERTY(L"TestCategory", L"WSLC") \
132 END_TEST_CLASS()
133
134 //
@@ -173,10 +185,8 @@ template <typename T>
185 class RegistryKeyChange
186 {
187 public:
176 - RegistryKeyChange(HKEY Hive, LPCWSTR Key, LPCWSTR Name, const T& Value) : m_value(Name)
188 + RegistryKeyChange(HKEY Hive, LPCWSTR Key, LPCWSTR Name, const T& Value) : m_hive(Hive), m_key(Key), m_value(Name)
189 {
178 - m_key = wsl::windows::common::registry::CreateKey(Hive, Key, KEY_ALL_ACCESS);
179 -
190 m_originalValue = Get();
191
192 Set(Value);
@@ -184,38 +194,63 @@ public:
194
195 ~RegistryKeyChange()
196 {
187 - if (m_key)
197 + if (m_key != nullptr)
198 {
199 + auto key = wsl::windows::common::registry::CreateKey(m_hive, m_key, KEY_ALL_ACCESS);
200 +
201 if (m_originalValue.has_value())
202 {
203 Set(m_originalValue.value());
204 }
205 else
206 {
195 - wsl::windows::common::registry::DeleteKeyValue(m_key.get(), m_value.c_str());
207 + wsl::windows::common::registry::DeleteKeyValue(key.get(), m_value.c_str());
208 }
209 }
210 }
211
212 + wil::unique_hkey OpenKey()
213 + {
214 + return wsl::windows::common::registry::CreateKey(m_hive, m_key, KEY_ALL_ACCESS);
215 + }
216 +
217 RegistryKeyChange(const RegistryKeyChange&) = delete;
201 - RegistryKeyChange(RegistryKeyChange&& other) = default;
202 - const RegistryKeyChange& operator=(RegistryKeyChange&& other)
218 + RegistryKeyChange(RegistryKeyChange&& other) noexcept :
219 + m_hive(other.m_hive), m_key(other.m_key), m_value(std::move(other.m_value)), m_originalValue(std::move(other.m_originalValue))
220 {
204 - m_key = std::move(other.m_key);
205 - m_value = std::move(other.m_value);
221 + other.m_hive = nullptr;
222 + other.m_key = nullptr;
223 + }
224 +
225 + RegistryKeyChange& operator=(RegistryKeyChange&& other)
226 + {
227 + if (this != &other)
228 + {
229 + m_hive = std::move(other.m_hive);
230 + m_key = std::move(other.m_key);
231 + m_value = std::move(other.m_value);
232 + m_originalValue = std::move(other.m_originalValue);
233 +
234 + other.m_hive = nullptr;
235 + other.m_key = nullptr;
236 + }
237 +
238 + return *this;
239 }
240
241 const RegistryKeyChange& operator=(RegistryKeyChange&) = delete;
242
243 void Set(const T& Value)
244 {
245 + auto key = wsl::windows::common::registry::CreateKey(m_hive, m_key, KEY_ALL_ACCESS);
246 +
247 if constexpr (std::is_same_v<std::remove_reference_t<T>, DWORD>)
248 {
214 - wsl::windows::common::registry::WriteDword(m_key.get(), nullptr, m_value.c_str(), Value);
249 + wsl::windows::common::registry::WriteDword(key.get(), nullptr, m_value.c_str(), Value);
250 }
251 else if constexpr (std::is_same_v<std::remove_reference_t<T>, std::wstring>)
252 {
218 - wsl::windows::common::registry::WriteString(m_key.get(), nullptr, m_value.c_str(), Value.c_str());
253 + wsl::windows::common::registry::WriteString(key.get(), nullptr, m_value.c_str(), Value.c_str());
254 }
255 else
256 {
@@ -225,11 +260,14 @@ public:
260
261 auto Get() const
262 {
263 +
264 + auto key = wsl::windows::common::registry::CreateKey(m_hive, m_key, KEY_ALL_ACCESS);
265 +
266 if constexpr (std::is_same_v<T, DWORD>)
267 {
268 DWORD Value = 0;
269 DWORD Size = sizeof(Value);
232 - const auto Result = RegGetValueW(m_key.get(), nullptr, m_value.c_str(), RRF_RT_REG_DWORD, nullptr, &Value, &Size);
270 + const auto Result = RegGetValueW(key.get(), nullptr, m_value.c_str(), RRF_RT_REG_DWORD, nullptr, &Value, &Size);
271 if (Result == ERROR_SUCCESS)
272 {
273 WI_ASSERT(Size == sizeof(Value));
@@ -246,7 +284,7 @@ public:
284 }
285 else if constexpr (std::is_same_v<std::remove_reference_t<T>, std::wstring>)
286 {
249 - return wsl::windows::common::registry::ReadOptionalString(m_key.get(), nullptr, m_value.c_str());
287 + return wsl::windows::common::registry::ReadOptionalString(key.get(), nullptr, m_value.c_str());
288 }
289 else
290 {
@@ -255,7 +293,8 @@ public:
293 }
294
295 private:
258 - wil::unique_hkey m_key;
296 + HKEY m_hive = nullptr;
297 + LPCWSTR m_key = nullptr;
298 std::wstring m_value;
299 std::optional<T> m_originalValue;
300 };
@@ -309,6 +348,34 @@ private:
348 LPCWSTR m_path{};
349 };
350
351 +class PartialHandleRead
352 +{
353 +public:
354 + NON_COPYABLE(PartialHandleRead);
355 + NON_MOVABLE(PartialHandleRead);
356 +
357 + PartialHandleRead(HANDLE Handle);
358 + ~PartialHandleRead();
359 +
360 + void Expect(const std::string& Expected);
361 + void ExpectConsume(const std::string& Expected);
362 + void ExpectClosed(DWORD Timeout = 60 * 1000);
363 +
364 + std::string ReadBytes(size_t Length);
365 + std::string ConsumeBytes(size_t Length);
366 +
367 + std::string GetData() const;
368 +
369 +private:
370 + void Run();
371 +
372 + HANDLE m_handle{};
373 + mutable std::mutex m_mutex;
374 + wil::unique_event m_exitEvent{wil::EventOptions::ManualReset};
375 + std::thread m_thread;
376 + std::string m_data;
377 +};
378 +
379 //
380 // Structs and enums.
381 //
@@ -427,7 +494,7 @@ std::vector<std::wstring> LxssSplitString(_In_ const std::wstring& string, _In_
494
495 void RestartWslService();
496
430 -wil::unique_handle GetNonElevatedToken();
497 +wil::unique_handle GetNonElevatedToken(TOKEN_TYPE Type = TokenPrimary);
498
499 std::wstring LxssWriteWslConfig(const std::wstring& Content);
500
@@ -522,4 +589,78 @@ void StopWslService();
589 std::optional<GUID> GetDistributionId(LPCWSTR Name);
590 wil::unique_hkey OpenDistributionKey(LPCWSTR Name);
591
525 -void ValidateOutput(LPCWSTR CommandLine, const std::wstring& ExpectedOutput, const std::wstring& ExpectedWarnings = L"", int ExitCode = -1);
\ No newline at end of file
592 +void ValidateOutput(LPCWSTR CommandLine, const std::wstring& ExpectedOutput, const std::wstring& ExpectedWarnings = L"", int ExitCode = -1);
593 +
594 +std::string ReadToString(SOCKET Handle);
595 +std::string ReadToString(HANDLE Handle);
596 +
597 +std::wstring ReadFileContent(const std::string& Path);
598 +std::wstring ReadFileContent(const std::wstring& Path);
599 +
600 +void WaitForOutput(wil::unique_handle handle, std::string_view targetValue, std::chrono::milliseconds timeout = 60s);
601 +
602 +std::string EscapeString(const std::string& Input);
603 +
604 +void VerifyPatternMatch(const std::string& Content, const std::string& Pattern);
605 +
606 +std::filesystem::path GetTestImagePath(std::string_view imageName);
607 +
608 +void ExpectHttpResponse(LPCWSTR Url, std::optional<int> expectedCode, bool retry = false);
609 +
610 +template <typename T>
611 +void VerifyAreEqualUnordered(const std::vector<T>& expected, const std::vector<T>& actual, const std::source_location& source = std::source_location::current())
612 +{
613 + std::map<T, size_t> expectedCounts;
614 + std::map<T, size_t> actualCounts;
615 +
616 + for (const auto& e : expected)
617 + {
618 + expectedCounts[e]++;
619 + }
620 +
621 + for (const auto& e : actual)
622 + {
623 + actualCounts[e]++;
624 + }
625 +
626 + std::wstring error;
627 +
628 + for (const auto& [value, count] : expectedCounts)
629 + {
630 + if (actualCounts[value] != count)
631 + {
632 + error += std::format(L"Value '{}' expected {} times but was found {} times.\n", value, count, actualCounts[value]);
633 + }
634 + }
635 +
636 + for (const auto& [value, count] : actualCounts)
637 + {
638 + if (expectedCounts.find(value) == expectedCounts.end())
639 + {
640 + error += std::format(L"Unexpected value found: '{}'", value);
641 + }
642 + }
643 +
644 + if (!error.empty())
645 + {
646 + error += std::format(L"Expected ({} elements):\n", expected.size());
647 + for (const auto& e : expected)
648 + {
649 + error += std::format(L"- {}\n", e);
650 + }
651 +
652 + error += std::format(L"Actual ({} elements):\n", actual.size());
653 +
654 + for (const auto& e : actual)
655 + {
656 + error += std::format(L"- {}\n", e);
657 + }
658 +
659 + error += std::format(L"Called from: {}", source);
660 +
661 + LogError("VerifyAreEqualUnordered failed: %ls", error.c_str());
662 + VERIFY_FAIL();
663 + }
664 +}
665 +
666 +void SetPathAccess(const std::filesystem::path& path, DWORD Permissions, ACCESS_MODE Mode);
test/windows/InstallerTests.cpp
+133 -1
@@ -719,7 +719,7 @@ class InstallerTests
719 ValidatePackageInstalledProperly();
720 }
721
722 - TEST_METHOD(InstallremovesStaleServiceRegistration)
722 + TEST_METHOD(InstallRemovesStaleServiceRegistration)
723 {
724 // Remove the MSI package.
725 UninstallMsi();
@@ -898,6 +898,47 @@ class InstallerTests
898 VERIFY_IS_FALSE(SfcIsKeyProtected(HKEY_LOCAL_MACHINE, keyPath, KEY_WOW64_64KEY));
899 }
900
901 + void ValidateDcatRegistration()
902 + {
903 + const auto versionValue =
904 + wsl::windows::common::registry::ReadString(HKEY_LOCAL_MACHINE, WIDEN(DCAT_REGISTRATION_KEY), L"Version");
905 + VERIFY_ARE_EQUAL(versionValue, WIDEN(WSL_PACKAGE_VERSION));
906 + }
907 +
908 + TEST_METHOD(InstallerRegistersWithDcat)
909 + {
910 + // Uninstalling should remove the registration
911 + UninstallMsi();
912 + VERIFY_IS_FALSE(IsMsiPackageInstalled());
913 + VERIFY_IS_FALSE(IsMsixInstalled());
914 +
915 + VERIFY_ARE_EQUAL(
916 + wsl::windows::common::registry::OpenKeyNoThrow(HKEY_LOCAL_MACHINE, WIDEN(DCAT_REGISTRATION_KEY), KEY_READ).second,
917 + HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
918 +
919 + // Installing should add the registration
920 + InstallMsi();
921 + VERIFY_IS_TRUE(IsMsiPackageInstalled());
922 + VERIFY_IS_TRUE(IsMsixInstalled());
923 +
924 + ValidateDcatRegistration();
925 + }
926 +
927 + TEST_METHOD(ServiceRemediatesDcatRegistration)
928 + {
929 + // Starting the service should create the registration if it is missing
930 + StopWslService();
931 + VERIFY_ARE_EQUAL(wsl::windows::common::registry::DeleteKey(HKEY_LOCAL_MACHINE, WIDEN(DCAT_REGISTRATION_KEY)), true);
932 + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--list"), 0);
933 + ValidateDcatRegistration();
934 +
935 + // Starting the service should fix the registration if needed
936 + StopWslService();
937 + wsl::windows::common::registry::WriteString(HKEY_LOCAL_MACHINE, WIDEN(DCAT_REGISTRATION_KEY), L"Version", L"1.0.0");
938 + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--list"), 0);
939 + ValidateDcatRegistration();
940 + }
941 +
942 void CallWslUpdateViaMsi()
943 {
944
@@ -1048,4 +1089,95 @@ class InstallerTests
1089 SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
1090 VerifyWslSettingsProtocolAssociationExistsWithRetry();
1091 }
1092 +
1093 + /*
1094 + TODO: Uncomment when the functionality is implemented in the SDK.
1095 + TEST_METHOD(WSLCInstall)
1096 + {
1097 + auto expectComponents = [](WslInstallComponent expected) {
1098 + WslInstallComponent components{};
1099 + VERIFY_SUCCEEDED(WslQueryMissingComponents(&components));
1100 +
1101 + VERIFY_ARE_EQUAL(components, expected);
1102 + };
1103 +
1104 + VERIFY_ARE_EQUAL(WslInstallComponents(WslInstallComponentWslPackage, nullptr, nullptr), E_INVALIDARG);
1105 +
1106 + // TODO: remove once 2.7.0 is released.
1107 + RegistryKeyChange<std::wstring> version(HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH "\\MSI", L"Version", L"2.7.0");
1108 +
1109 + expectComponents(WslInstallComponentNone);
1110 +
1111 + // Validate that a package < 2.7 is handled correctly.
1112 + {
1113 + version.Set(L"2.6.0");
1114 + expectComponents(WslInstallComponentWslPackage);
1115 + }
1116 +
1117 + version.Set(L"2.7.0");
1118 +
1119 + // Validate that a missing package is detected.
1120 + expectComponents(WslInstallComponentNone);
1121 + UninstallMsi();
1122 +
1123 + expectComponents(WslInstallComponentWslPackage);
1124 +
1125 + {
1126 + UniqueWebServer fileServer(L"http://127.0.0.1:12346/", std::filesystem::path(m_msiPath));
1127 + VERIFY_SUCCEEDED(WslSetPackageUrl(L"http://127.0.0.1:12346/"));
1128 +
1129 + WslInstallComponent progressedComponents{};
1130 + auto callback = [](WslInstallComponent Component, uint64_t progress, uint64_t total, void* Context) {
1131 + *reinterpret_cast<WslInstallComponent*>(Context) |= Component;
1132 + };
1133 +
1134 + VERIFY_SUCCEEDED(WslInstallComponents(WslInstallComponentWslPackage, callback, &progressedComponents));
1135 + VERIFY_ARE_EQUAL(progressedComponents, WslInstallComponentWslPackage);
1136 +
1137 + ValidateInstalledVersion(WIDEN(WSL_PACKAGE_VERSION));
1138 + version.Set(L"2.7.0");
1139 +
1140 + expectComponents(WslInstallComponentNone);
1141 +
1142 + progressedComponents = WslInstallComponentNone;
1143 + VERIFY_ARE_EQUAL(WslInstallComponents(WslInstallComponentVMPOC, callback, &progressedComponents),
1144 + HRESULT_FROM_WIN32(ERROR_SUCCESS_REBOOT_REQUIRED)); VERIFY_ARE_EQUAL(progressedComponents, WslInstallComponentVMPOC);
1145 +
1146 + progressedComponents = WslInstallComponentNone;
1147 + VERIFY_ARE_EQUAL(WslInstallComponents(WslInstallComponentWslOC, callback, &progressedComponents),
1148 + HRESULT_FROM_WIN32(ERROR_SUCCESS_REBOOT_REQUIRED)); VERIFY_ARE_EQUAL(progressedComponents, WslInstallComponentWslOC);
1149 + }
1150 +
1151 + {
1152 + VERIFY_SUCCEEDED(WslSetPackageUrl(L"http://127.0.0.1:12346/"));
1153 + VERIFY_ARE_EQUAL(WslInstallComponents(WslInstallComponentWslPackage, nullptr, nullptr), WININET_E_CANNOT_CONNECT);
1154 + }
1155 + }
1156 +
1157 + // This test case requires a machine without the OC's enabled.
1158 + TEST_METHOD(WSLCInstallManual)
1159 + {
1160 + WslInstallComponent components{};
1161 + VERIFY_SUCCEEDED(WslQueryMissingComponents(&components));
1162 +
1163 + if (!WI_IsAnyFlagSet(components, WslInstallComponentWslOC | WslInstallComponentVMPOC))
1164 + {
1165 + LogSkipped("OC are installed, skipping test. Flags: %i", components);
1166 + return;
1167 + }
1168 +
1169 + auto expectedComponents = WslInstallComponentVMPOC;
1170 + WI_SetFlagIf(expectedComponents, WslInstallComponentWslOC, !wsl::windows::common::helpers::IsWindows11OrAbove());
1171 +
1172 + VERIFY_ARE_EQUAL(components, expectedComponents);
1173 +
1174 + WslInstallComponent progressedComponents{};
1175 + auto callback = [](WslInstallComponent Component, uint64_t progress, uint64_t total, void* Context) {
1176 + *reinterpret_cast<WslInstallComponent*>(Context) |= Component;
1177 + };
1178 +
1179 + VERIFY_ARE_EQUAL(WslInstallComponents(components, callback, &progressedComponents),
1180 + HRESULT_FROM_WIN32(ERROR_SUCCESS_REBOOT_REQUIRED)); VERIFY_ARE_EQUAL(progressedComponents, expectedComponents);
1181 + }
1182 + */
1183 };
\ No newline at end of file
test/windows/NetworkTests.cpp
+3
@@ -3868,6 +3868,9 @@ class MirroredTests
3868
3869 WSL2_TEST_METHOD(LoopbackExplicit)
3870 {
3871 + // TODO: re-enable once OS build 29555 loopback regression is resolved.
3872 + SKIP_TEST_UNSTABLE();
3873 +
3874 MIRRORED_NETWORKING_TEST_ONLY();
3875
3876 m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
test/windows/UnitTests.cpp
+97 -7
@@ -965,7 +965,7 @@ class UnitTests
965 VERIFY_IS_FALSE(!vhdFile);
966 }
967
968 - auto validateOutput = [](LPCWSTR commandLine, LPCWSTR expectedOutput, DWORD expectedExitCode = -1) {
968 + auto validateOutput = [](LPCWSTR commandLine, const std::wstring& expectedOutput, DWORD expectedExitCode = -1) {
969 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(commandLine, expectedExitCode);
970 VERIFY_ARE_EQUAL(expectedOutput, out);
971 VERIFY_ARE_EQUAL(L"", err);
@@ -973,10 +973,22 @@ class UnitTests
973
974 auto version = LxsstuVmMode() ? 2 : 1;
975 auto commandLine = std::format(L"--import dummy {} {} --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, tarFileName, version);
976 - validateOutput(
977 - commandLine.c_str(),
978 - L"The supplied install location is already in use.\r\n"
979 - L"Error code: Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS\r\n");
976 + if (LxsstuVmMode())
977 + {
978 + validateOutput(
979 + commandLine.c_str(),
980 + std::format(
981 + L"Failed to create disk '{}ext4.vhdx': The file exists. \r\n"
982 + L"Error code: Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS\r\n",
983 + LXSST_IMPORT_DISTRO_TEST_DIR));
984 + }
985 + else
986 + {
987 + validateOutput(
988 + commandLine.c_str(),
989 + L"The file exists. \r\n"
990 + L"Error code: Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS\r\n");
991 + }
992
993 commandLine = std::format(L"--import dummy {} {} --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, vhdFileName, version);
994 validateOutput(commandLine.c_str(), L"This looks like a VHD file. Use --vhd to import a VHD instead of a tar.\r\n");
@@ -990,6 +1002,25 @@ class UnitTests
1002 L"Error code: Wsl/Service/RegisterDistro/WSL_E_WSL2_NEEDED\r\n");
1003 }
1004
1005 + //
1006 + // Verify that importing a distribution with a different name into the same path as an
1007 + // already registered distribution (test_distro) returns the path-already-exists error.
1008 + //
1009 +
1010 + {
1011 + const auto distroKey = OpenDistributionKey(LXSS_DISTRO_NAME_TEST_L);
1012 + VERIFY_IS_TRUE(!!distroKey);
1013 +
1014 + auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
1015 + VERIFY_IS_FALSE(basePath.empty());
1016 +
1017 + commandLine = std::format(L"--import path-conflict-distro \"{}\" \"{}\" --version {}", basePath, tarFileName, version);
1018 + validateOutput(
1019 + commandLine.c_str(),
1020 + L"The supplied install location is already in use.\r\n"
1021 + L"Error code: Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS\r\n");
1022 + }
1023 +
1024 //
1025 // Create and import a new distro that where /bin/sh is an absolute symlink.
1026 //
@@ -5440,7 +5471,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND\r\n",
5471
5472 VERIFY_ARE_EQUAL(
5473 out,
5443 - L"A distribution with the supplied name already exists. Use --name to chose a different name.\r\n"
5474 + L"Cannot create a file when that file already exists. \r\n"
5475 L"Error code: Wsl/InstallDistro/ERROR_ALREADY_EXISTS\r\n");
5476
5477 VERIFY_ARE_EQUAL(err, L"");
@@ -5451,7 +5482,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND\r\n",
5482
5483 VERIFY_ARE_EQUAL(
5484 out,
5454 - L"A distribution with the supplied name already exists. Use --name to chose a different name.\r\n"
5485 + L"Cannot create a file when that file already exists. \r\n"
5486 L"Error code: Wsl/InstallDistro/ERROR_ALREADY_EXISTS\r\n");
5487
5488 VERIFY_ARE_EQUAL(err, L"");
@@ -6038,6 +6069,7 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
6069 TerminateDistribution();
6070
6071 const auto nonElevatedToken = GetNonElevatedToken();
6072 +
6073 VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(L"echo dummy", nullptr, nullptr, nullptr, nonElevatedToken.get()));
6074 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"mountpoint /mnt/c", 0u);
6075 VERIFY_ARE_EQUAL(out, L"/mnt/c is a mountpoint\n");
@@ -6531,6 +6563,64 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
6563 VERIFY_ARE_EQUAL(BytesToHex({0xFF, 0xFF}), L"0xffff");
6564 }
6565
6566 + TEST_METHOD(HexToBytes)
6567 + {
6568 + using wsl::windows::common::string::BytesToHex;
6569 + using wsl::windows::common::string::HexToBytes;
6570 + using ByteVec = std::vector<BYTE>;
6571 +
6572 + // Wide string with 0x prefix
6573 + VERIFY_ARE_EQUAL(HexToBytes(L"0xdeadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
6574 +
6575 + // Narrow string with 0x prefix
6576 + VERIFY_ARE_EQUAL(HexToBytes("0xdeadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
6577 +
6578 + // Wide string without prefix
6579 + VERIFY_ARE_EQUAL(HexToBytes(L"deadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
6580 +
6581 + // Narrow string without prefix
6582 + VERIFY_ARE_EQUAL(HexToBytes("deadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
6583 +
6584 + // Empty string
6585 + VERIFY_ARE_EQUAL(HexToBytes(L""), (ByteVec{}));
6586 +
6587 + // Single byte
6588 + VERIFY_ARE_EQUAL(HexToBytes(L"0x0f"), (ByteVec{0x0F}));
6589 +
6590 + // Uppercase hex digits
6591 + VERIFY_ARE_EQUAL(HexToBytes(L"0xDEADBEEF"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
6592 +
6593 + // Round-trip: BytesToHex -> HexToBytes
6594 + const ByteVec original = {0x01, 0x23, 0xAB};
6595 + VERIFY_ARE_EQUAL(HexToBytes(BytesToHex(original)), original);
6596 +
6597 + // Odd-length string (after stripping "0x") throws E_INVALIDARG
6598 + bool threw = false;
6599 + try
6600 + {
6601 + HexToBytes(L"0xabc");
6602 + }
6603 + catch (const wil::ResultException& e)
6604 + {
6605 + VERIFY_ARE_EQUAL(e.GetErrorCode(), E_INVALIDARG);
6606 + threw = true;
6607 + }
6608 + VERIFY_IS_TRUE(threw);
6609 +
6610 + // Invalid hex character throws E_INVALIDARG
6611 + threw = false;
6612 + try
6613 + {
6614 + HexToBytes(L"0xZZ");
6615 + }
6616 + catch (const wil::ResultException& e)
6617 + {
6618 + VERIFY_ARE_EQUAL(e.GetErrorCode(), E_INVALIDARG);
6619 + threw = true;
6620 + }
6621 + VERIFY_IS_TRUE(threw);
6622 + }
6623 +
6624 WSL2_TEST_METHOD(InteractiveMount)
6625 {
6626 // Add a fake interactive mount helper.
test/windows/WSLCTests.cpp new
+8217
@@ -0,0 +1,8217 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains test cases for the WSLC API.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "Common.h"
17 +#include "wslc.h"
18 +#include "WSLCProcessLauncher.h"
19 +#include "WSLCContainerLauncher.h"
20 +#include "WslCoreFilesystem.h"
21 +#include <nlohmann/json.hpp>
22 +
23 +using namespace std::literals::chrono_literals;
24 +using namespace wsl::windows::common::registry;
25 +using wsl::windows::common::RunningWSLCContainer;
26 +using wsl::windows::common::RunningWSLCProcess;
27 +using wsl::windows::common::WSLCContainerLauncher;
28 +using wsl::windows::common::WSLCProcessLauncher;
29 +using wsl::windows::common::relay::OverlappedIOHandle;
30 +using wsl::windows::common::relay::WriteHandle;
31 +using namespace wsl::windows::common::wslutil;
32 +
33 +extern std::wstring g_testDataPath;
34 +extern bool g_fastTestRun;
35 +
36 +class WSLCTests
37 +{
38 + WSLC_TEST_CLASS(WSLCTests)
39 +
40 + WSADATA m_wsadata;
41 + std::filesystem::path m_storagePath;
42 + WSLCSessionSettings m_defaultSessionSettings{};
43 + wil::com_ptr<IWSLCSession> m_defaultSession;
44 + static inline auto c_testSessionName = L"wslc-test";
45 +
46 + void LoadTestImage(std::string_view imageName, IWSLCSession* session = nullptr)
47 + {
48 + std::filesystem::path imagePath = GetTestImagePath(imageName);
49 + wil::unique_hfile imageFile{
50 + CreateFileW(imagePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
51 + THROW_LAST_ERROR_IF(!imageFile);
52 +
53 + LARGE_INTEGER fileSize{};
54 + THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize));
55 +
56 + THROW_IF_FAILED(
57 + (session ? session : m_defaultSession.get())->LoadImage(ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart));
58 + }
59 +
60 + TEST_CLASS_SETUP(TestClassSetup)
61 + {
62 + THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata));
63 +
64 + // The WSLC SDK tests use this same storage to reduce pull overhead.
65 + m_storagePath = std::filesystem::current_path() / "test-storage";
66 + m_defaultSessionSettings = GetDefaultSessionSettings(c_testSessionName, true, WSLCNetworkingModeVirtioProxy);
67 + m_defaultSession = CreateSession(m_defaultSessionSettings);
68 +
69 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
70 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, &images, images.size_address<ULONG>()));
71 +
72 + auto hasImage = [&](const std::string& imageName) {
73 + return std::ranges::any_of(
74 + images.get(), images.get() + images.size(), [&](const auto& e) { return e.Image == imageName; });
75 + };
76 +
77 + if (!hasImage("debian:latest"))
78 + {
79 + LoadTestImage("debian:latest");
80 + }
81 +
82 + if (!hasImage("python:3.12-alpine"))
83 + {
84 + LoadTestImage("python:3.12-alpine");
85 + }
86 +
87 + if (!hasImage("hello-world:latest"))
88 + {
89 + LoadTestImage("hello-world:latest");
90 + }
91 +
92 + if (!hasImage("alpine:latest"))
93 + {
94 + LoadTestImage("alpine:latest");
95 + }
96 +
97 + if (!hasImage("wslc-registry:latest"))
98 + {
99 + LoadTestImage("wslc-registry:latest");
100 + }
101 +
102 + PruneResult result;
103 + VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result));
104 + if (result.result.ContainersCount > 0)
105 + {
106 + LogInfo("Pruned %lu containers", result.result.ContainersCount);
107 + }
108 +
109 + return true;
110 + }
111 +
112 + TEST_CLASS_CLEANUP(TestClassCleanup)
113 + {
114 + m_defaultSession.reset();
115 +
116 + // Keep the VHD when running in -f mode, to speed up subsequent test runs.
117 + if (!g_fastTestRun && !m_storagePath.empty())
118 + {
119 + std::error_code error;
120 + std::filesystem::remove_all(m_storagePath, error);
121 + if (error)
122 + {
123 + LogError("Failed to cleanup storage path %ws: %hs", m_storagePath.c_str(), error.message().c_str());
124 + }
125 + }
126 +
127 + return true;
128 + }
129 +
130 + WSLCSessionSettings GetDefaultSessionSettings(LPCWSTR Name, bool enableStorage = false, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone)
131 + {
132 + WSLCSessionSettings settings{};
133 + settings.DisplayName = Name;
134 + settings.CpuCount = 4;
135 + settings.MemoryMb = 2048;
136 + settings.BootTimeoutMs = 30 * 1000;
137 + settings.StoragePath = enableStorage ? m_storagePath.c_str() : nullptr;
138 + settings.MaximumStorageSizeMb = 1024 * 20; // 20GB.
139 + settings.NetworkingMode = networkingMode;
140 +
141 + return settings;
142 + }
143 +
144 + auto ResetTestSession()
145 + {
146 + m_defaultSession.reset();
147 +
148 + return wil::scope_exit([this]() { m_defaultSession = CreateSession(m_defaultSessionSettings); });
149 + }
150 +
151 + static wil::com_ptr<IWSLCSessionManager> OpenSessionManager()
152 + {
153 + wil::com_ptr<IWSLCSessionManager> sessionManager;
154 + VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
155 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
156 +
157 + return sessionManager;
158 + }
159 +
160 + wil::com_ptr<IWSLCSession> CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags Flags = WSLCSessionFlagsNone)
161 + {
162 + const auto sessionManager = OpenSessionManager();
163 +
164 + wil::com_ptr<IWSLCSession> session;
165 +
166 + VERIFY_SUCCEEDED(sessionManager->CreateSession(&sessionSettings, Flags, &session));
167 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
168 +
169 + WSLCSessionState state{};
170 + VERIFY_SUCCEEDED(session->GetState(&state));
171 + VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning);
172 +
173 + return session;
174 + }
175 +
176 + RunningWSLCContainer OpenContainer(IWSLCSession* session, const std::string& name)
177 + {
178 + wil::com_ptr<IWSLCContainer> rawContainer;
179 + VERIFY_SUCCEEDED(session->OpenContainer(name.c_str(), &rawContainer));
180 +
181 + return RunningWSLCContainer(std::move(rawContainer), {});
182 + }
183 +
184 + std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(const std::string& username = {}, const std::string& password = {}, USHORT port = 5000)
185 + {
186 + std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
187 + if (!username.empty())
188 + {
189 + env.push_back(std::format("USERNAME={}", username));
190 + env.push_back(std::format("PASSWORD={}", password));
191 + }
192 +
193 + WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env);
194 + launcher.SetEntrypoint({"/entrypoint.sh"});
195 + launcher.AddPort(port, port, AF_INET);
196 +
197 + auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
198 +
199 + auto registryAddress = std::format("127.0.0.1:{}", port);
200 + auto registryUrl = std::format(L"http://{}", registryAddress);
201 + ExpectHttpResponse(registryUrl.c_str(), 200, true);
202 +
203 + return {std::move(container), std::move(registryAddress)};
204 + }
205 +
206 + std::string PushImageToRegistry(const std::string& imageName, const std::string& registryAddress, const std::string& registryAuth)
207 + {
208 + auto [repo, tag] = ParseImage(imageName);
209 + auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest"));
210 + auto registryRepo = std::format("{}/{}", registryAddress, repo);
211 + auto registryTag = tag.value_or("latest");
212 +
213 + WSLCTagImageOptions tagOptions{};
214 + tagOptions.Image = imageName.c_str();
215 + tagOptions.Repo = registryRepo.c_str();
216 + tagOptions.Tag = registryTag.c_str();
217 +
218 + // Tag the image with the registry address so it can be pushed.
219 + VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
220 +
221 + // Ensures the tag is removed to allow tests to try to push or pull the same image again.
222 + auto cleanup = wil::scope_exit_log(
223 + WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(DeleteImageNoThrow(registryImage, WSLCDeleteImageFlagsNone).first); });
224 +
225 + VERIFY_SUCCEEDED(m_defaultSession->PushImage(registryImage.c_str(), registryAuth.c_str(), nullptr));
226 +
227 + return registryImage;
228 + }
229 +
230 + WSLC_TEST_METHOD(GetVersion)
231 + {
232 + wil::com_ptr<IWSLCSessionManager> sessionManager;
233 + VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
234 +
235 + WSLCVersion version{};
236 +
237 + VERIFY_SUCCEEDED(sessionManager->GetVersion(&version));
238 +
239 + VERIFY_ARE_EQUAL(version.Major, WSL_PACKAGE_VERSION_MAJOR);
240 + VERIFY_ARE_EQUAL(version.Minor, WSL_PACKAGE_VERSION_MINOR);
241 + VERIFY_ARE_EQUAL(version.Revision, WSL_PACKAGE_VERSION_REVISION);
242 + }
243 +
244 + static RunningWSLCProcess::ProcessResult RunCommand(IWSLCSession* session, const std::vector<std::string>& command, int timeout = 600000)
245 + {
246 + WSLCProcessLauncher process(command[0], command);
247 +
248 + return process.Launch(*session).WaitAndCaptureOutput(timeout);
249 + }
250 +
251 + static RunningWSLCProcess::ProcessResult ExpectCommandResult(
252 + IWSLCSession* session, const std::vector<std::string>& command, int expectResult, int timeout = 600000)
253 + {
254 + auto result = RunCommand(session, command, timeout);
255 +
256 + if (result.Code != expectResult)
257 + {
258 + auto cmd = wsl::shared::string::Join(command, ' ');
259 + LogError(
260 + "Command: %hs didn't return expected code (%i). ExitCode: %i, Stdout: '%hs', Stderr: '%hs'",
261 + cmd.c_str(),
262 + expectResult,
263 + result.Code,
264 + result.Output[1].c_str(),
265 + result.Output[2].c_str());
266 + }
267 +
268 + return result;
269 + }
270 +
271 + void ValidateProcessOutput(RunningWSLCProcess& process, const std::map<int, std::string>& expectedOutput, int expectedResult = 0, DWORD Timeout = INFINITE)
272 + {
273 + auto result = process.WaitAndCaptureOutput(Timeout);
274 +
275 + if (result.Code != expectedResult)
276 + {
277 + LogError(
278 + "Command didn't return expected code (%i). ExitCode: %i, Stdout: '%hs', Stderr: '%hs'",
279 + expectedResult,
280 + result.Code,
281 + EscapeString(result.Output[1]).c_str(),
282 + EscapeString(result.Output[2]).c_str());
283 +
284 + return;
285 + }
286 +
287 + for (const auto& [fd, expected] : expectedOutput)
288 + {
289 + auto it = result.Output.find(fd);
290 + if (it == result.Output.end())
291 + {
292 + LogError("Expected output on fd %i, but none found.", fd);
293 + return;
294 + }
295 +
296 + if (it->second != expected)
297 + {
298 + LogError(
299 + "Unexpected output on fd %i. Expected: '%hs', Actual: '%hs'",
300 + fd,
301 + EscapeString(expected).c_str(),
302 + EscapeString(it->second).c_str());
303 +
304 + return;
305 + }
306 + }
307 + }
308 +
309 + void ValidateContainerOutput(RunningWSLCContainer& container, const std::map<int, std::string>& expectedOutput, int expectedResult = 0, DWORD timeout = INFINITE)
310 + {
311 + auto initProcess = container.GetInitProcess();
312 + ValidateProcessOutput(initProcess, expectedOutput, expectedResult, timeout);
313 + }
314 +
315 + void ValidateContainerOutput(WSLCContainerLauncher& launcher, const std::map<int, std::string>& expectedOutput, int expectedResult = 0, DWORD timeout = INFINITE)
316 + {
317 + auto container = launcher.Launch(*m_defaultSession);
318 + ValidateContainerOutput(container, expectedOutput, expectedResult, timeout);
319 + }
320 +
321 + void ExpectMount(IWSLCSession* session, const std::string& target, const std::optional<std::string>& options)
322 + {
323 + auto cmd = std::format("set -o pipefail ; findmnt '{}' | tail -n 1", target);
324 + auto result = ExpectCommandResult(session, {"/bin/sh", "-c", cmd}, options.has_value() ? 0 : 1);
325 +
326 + const auto& output = result.Output[1];
327 + const auto& error = result.Output[2];
328 +
329 + if (result.Code != (options.has_value() ? 0 : 1))
330 + {
331 + LogError("%hs failed. code=%i, output: %hs, error: %hs", cmd.c_str(), result.Code, output.c_str(), error.c_str());
332 + VERIFY_FAIL();
333 + }
334 +
335 + if (options.has_value() && !PathMatchSpecA(output.c_str(), options->c_str()))
336 + {
337 + std::wstring message = std::format(L"Output: '{}' didn't match pattern: '{}'", output, options.value());
338 + VERIFY_FAIL(message.c_str());
339 + }
340 + }
341 +
342 + WSLC_TEST_METHOD(ListSessionsReturnsSessionWithDisplayName)
343 + {
344 + auto sessionManager = OpenSessionManager();
345 +
346 + // Act: list sessions
347 + {
348 + wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
349 + VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
350 +
351 + // Assert
352 + VERIFY_ARE_EQUAL(sessions.size(), 1u);
353 + const auto& info = sessions[0];
354 +
355 + // SessionId is implementation detail (starts at 1), so we only assert DisplayName here.
356 + VERIFY_ARE_EQUAL(std::wstring(info.DisplayName), c_testSessionName);
357 + }
358 +
359 + // List multiple sessions.
360 + {
361 + auto session2 = CreateSession(GetDefaultSessionSettings(L"wslc-test-list-2"));
362 +
363 + wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
364 + VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
365 +
366 + VERIFY_ARE_EQUAL(sessions.size(), 2);
367 +
368 + std::vector<std::wstring> displayNames;
369 + for (const auto& e : sessions)
370 + {
371 + displayNames.push_back(e.DisplayName);
372 + }
373 +
374 + std::ranges::sort(displayNames);
375 +
376 + VERIFY_ARE_EQUAL(displayNames[0], c_testSessionName);
377 + VERIFY_ARE_EQUAL(displayNames[1], L"wslc-test-list-2");
378 + }
379 + }
380 +
381 + WSLC_TEST_METHOD(OpenSessionByNameFindsExistingSession)
382 + {
383 + auto sessionManager = OpenSessionManager();
384 +
385 + // Act: open by the same display name
386 + wil::com_ptr<IWSLCSession> opened;
387 + VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(c_testSessionName, &opened));
388 + VERIFY_IS_NOT_NULL(opened.get());
389 +
390 + // And verify we get ERROR_NOT_FOUND for a nonexistent name
391 + wil::com_ptr<IWSLCSession> notFound;
392 + auto hr = sessionManager->OpenSessionByName(L"this-name-does-not-exist", &notFound);
393 + VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
394 + }
395 +
396 + WSLC_TEST_METHOD(CreateSessionValidation)
397 + {
398 + auto sessionManager = OpenSessionManager();
399 +
400 + // Reject NULL DisplayName.
401 + {
402 + auto settings = GetDefaultSessionSettings(nullptr);
403 + wil::com_ptr<IWSLCSession> session;
404 + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME);
405 + }
406 +
407 + // Reject DisplayName at exact boundary (no room for null terminator).
408 + {
409 + std::wstring boundaryName(std::size(WSLCSessionInformation{}.DisplayName), L'x');
410 + auto settings = GetDefaultSessionSettings(boundaryName.c_str());
411 + wil::com_ptr<IWSLCSession> session;
412 + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME);
413 + }
414 +
415 + // Reject too long DisplayName.
416 + {
417 + std::wstring longName(std::size(WSLCSessionInformation{}.DisplayName) + 1, L'x');
418 + auto settings = GetDefaultSessionSettings(longName.c_str());
419 + wil::com_ptr<IWSLCSession> session;
420 + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME);
421 + }
422 +
423 + // Validate that creating a session on a non-existing storage fails if WSLCSessionStorageFlagsNoCreate is set.
424 + {
425 + auto settings = GetDefaultSessionSettings(L"storage-not-found");
426 + settings.StoragePath = L"C:\\does-not-exist";
427 + settings.StorageFlags = WSLCSessionStorageFlagsNoCreate;
428 + wil::com_ptr<IWSLCSession> session;
429 + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
430 + }
431 +
432 + // Reject invalid storage flags.
433 + {
434 + auto settings = GetDefaultSessionSettings(L"invalid-storage-flags");
435 + settings.StorageFlags = static_cast<WSLCSessionStorageFlags>(0x2);
436 + wil::com_ptr<IWSLCSession> session;
437 + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG);
438 + }
439 + }
440 +
441 + struct VmInfo
442 + {
443 + std::wstring Id;
444 + std::wstring Owner;
445 + };
446 +
447 + // Returns VM info (Id + Owner) for all running VMs via hcsdiag.
448 + static std::vector<VmInfo> ListVms()
449 + {
450 + wsl::windows::common::SubProcess process(nullptr, L"hcsdiag list -raw");
451 + auto output = process.RunAndCaptureOutput(10000);
452 +
453 + std::vector<VmInfo> vms;
454 + auto json = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(output.Stdout), nullptr, false);
455 + if (!json.is_array())
456 + {
457 + return vms;
458 + }
459 +
460 + for (const auto& entry : json)
461 + {
462 + if (entry.contains("Owner") && entry["Owner"].is_string() && entry.contains("Id") && entry["Id"].is_string())
463 + {
464 + vms.push_back(
465 + {wsl::shared::string::MultiByteToWide(entry["Id"].get<std::string>()),
466 + wsl::shared::string::MultiByteToWide(entry["Owner"].get<std::string>())});
467 + }
468 + }
469 +
470 + return vms;
471 + }
472 +
473 + WSLC_TEST_METHOD(VmOwnerMatchesSessionDisplayName)
474 + {
475 + // The default session (c_testSessionName) is already running from class setup.
476 + // Verify its display name appears as a VM owner in hcsdiag output.
477 + auto vms = ListVms();
478 +
479 + auto found = std::ranges::find_if(vms, [](const auto& vm) { return vm.Owner == c_testSessionName; });
480 + if (found == vms.end())
481 + {
482 + LogError("Expected VM owner '%ws' not found. Owners:", c_testSessionName);
483 + for (const auto& vm : vms)
484 + {
485 + LogError(" '%ws'", vm.Owner.c_str());
486 + }
487 +
488 + VERIFY_FAIL();
489 + }
490 + }
491 +
492 + void ExpectImagePresent(IWSLCSession& Session, const char* Image, bool Present = true)
493 + {
494 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
495 + THROW_IF_FAILED(Session.ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
496 +
497 + std::vector<std::string> tags;
498 + for (const auto& e : images)
499 + {
500 + tags.push_back(e.Image);
501 + }
502 +
503 + auto found = std::ranges::find(tags, Image) != tags.end();
504 + if (Present != found)
505 + {
506 + LogError("Image presence check failed for image: %hs, images: %hs", Image, wsl::shared::string::Join(tags, ',').c_str());
507 + VERIFY_FAIL();
508 + }
509 + }
510 +
511 + std::pair<HRESULT, wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation>> DeleteImageNoThrow(const std::string& Image, DWORD Flags)
512 + {
513 + WSLCDeleteImageOptions options{};
514 + options.Image = Image.c_str();
515 + options.Flags = Flags;
516 + wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
517 + auto hr = m_defaultSession->DeleteImage(&options, deletedImages.addressof(), deletedImages.size_address<ULONG>());
518 + return {hr, std::move(deletedImages)};
519 + }
520 +
521 + wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> DeleteImage(const std::string& Image, DWORD Flags)
522 + {
523 + auto [hr, deletedImages] = DeleteImageNoThrow(Image, Flags);
524 + VERIFY_SUCCEEDED(hr);
525 +
526 + return std::move(deletedImages);
527 + }
528 +
529 + WSLC_TEST_METHOD(PullImage)
530 + {
531 + {
532 + // Start a local registry without auth and push hello-world:latest to it.
533 + auto [registryContainer, registryAddress] = StartLocalRegistry();
534 +
535 + auto image = PushImageToRegistry("hello-world:latest", registryAddress, BuildRegistryAuthHeader("", ""));
536 + ExpectImagePresent(*m_defaultSession, image.c_str(), false);
537 +
538 + VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr));
539 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(DeleteImageNoThrow(image, WSLCDeleteImageFlagsForce).first); });
540 +
541 + // Verify that the image is in the list of images.
542 + ExpectImagePresent(*m_defaultSession, image.c_str());
543 + WSLCContainerLauncher launcher(image, "wslc-pull-image-container");
544 +
545 + auto container = launcher.Launch(*m_defaultSession);
546 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
547 +
548 + VERIFY_ARE_EQUAL(0, result.Code);
549 + VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
550 + }
551 +
552 + {
553 + std::wstring expectedError =
554 + L"pull access denied for does-not, repository does not exist or may require 'docker login': denied: requested "
555 + L"access to the resource is denied";
556 +
557 + VERIFY_ARE_EQUAL(m_defaultSession->PullImage("does-not:exist", nullptr, nullptr), WSLC_E_IMAGE_NOT_FOUND);
558 + ValidateCOMErrorMessage(expectedError.c_str());
559 + }
560 +
561 + // Validate that PullImage() returns the appropriate error if the session is terminated.
562 + {
563 + VERIFY_SUCCEEDED(m_defaultSession->Terminate());
564 +
565 + auto cleanup = wil::scope_exit([&]() {
566 + ResetTestSession(); // Reopen the test session since the session was terminated.
567 + });
568 +
569 + VERIFY_ARE_EQUAL(m_defaultSession->PullImage("hello-world:linux", nullptr, nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
570 + }
571 + }
572 +
573 + WSLC_TEST_METHOD(PullImageAdvanced)
574 + {
575 + // Start a local registry without auth to avoid Docker Hub rate limits.
576 + auto [registryContainer, registryAddress] = StartLocalRegistry();
577 + auto auth = BuildRegistryAuthHeader("", "");
578 +
579 + auto validatePull = [&](const std::string& sourceImage) {
580 + // Push the source image to the local registry.
581 + auto registryImage = PushImageToRegistry(sourceImage, registryAddress, auth);
582 + ExpectImagePresent(*m_defaultSession, registryImage.c_str(), false);
583 +
584 + VERIFY_SUCCEEDED(m_defaultSession->PullImage(registryImage.c_str(), nullptr, nullptr));
585 +
586 + auto cleanup =
587 + wil::scope_exit([&]() { LOG_IF_FAILED(DeleteImageNoThrow(registryImage, WSLCDeleteImageFlagsForce).first); });
588 +
589 + ExpectImagePresent(*m_defaultSession, registryImage.c_str());
590 + };
591 +
592 + validatePull("debian:latest");
593 + validatePull("alpine:latest");
594 + validatePull("hello-world:latest");
595 + }
596 +
597 + WSLC_TEST_METHOD(PullImageFromDockerHub)
598 + {
599 + SKIP_TEST_UNSTABLE();
600 +
601 + auto validatePull = [&](const std::string& Image, const std::optional<std::string>& ExpectedTag = {}) {
602 + VERIFY_SUCCEEDED(m_defaultSession->PullImage(Image.c_str(), nullptr, nullptr));
603 +
604 + auto cleanup = wil::scope_exit(
605 + [&]() { LOG_IF_FAILED(DeleteImageNoThrow(ExpectedTag.value_or(Image), WSLCDeleteImageFlagsForce).first); });
606 +
607 + if (!ExpectedTag.has_value())
608 + {
609 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
610 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
611 +
612 + for (const auto& e : images)
613 + {
614 + wil::unique_cotaskmem_ansistring json;
615 + VERIFY_SUCCEEDED(m_defaultSession->InspectImage(e.Hash, &json));
616 +
617 + auto parsed = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectImage>(json.get());
618 +
619 + for (const auto& repoTag : parsed.RepoDigests.value_or({}))
620 + {
621 + if (Image == repoTag)
622 + {
623 + return;
624 + }
625 + }
626 + }
627 +
628 + LogError("Expected digest '%hs' not found ", Image.c_str());
629 +
630 + VERIFY_FAIL();
631 + }
632 + else
633 + {
634 + ExpectImagePresent(*m_defaultSession, ExpectedTag->c_str());
635 + }
636 + };
637 +
638 + validatePull("ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30", {});
639 + validatePull("ubuntu", "ubuntu:latest");
640 + validatePull("debian:bookworm", "debian:bookworm");
641 + validatePull("pytorch/pytorch", "pytorch/pytorch:latest");
642 + validatePull("registry.k8s.io/pause:3.2", "registry.k8s.io/pause:3.2");
643 +
644 + // Validate that PullImage() fails appropriately when the session runs out of space.
645 + {
646 + auto settings = GetDefaultSessionSettings(L"wslc-pull-image-out-of-space", false);
647 + settings.NetworkingMode = WSLCNetworkingModeVirtioProxy;
648 + settings.MemoryMb = 1024;
649 + auto session = CreateSession(settings);
650 +
651 + VERIFY_ARE_EQUAL(session->PullImage("pytorch/pytorch", nullptr, nullptr), E_FAIL);
652 +
653 + ValidateCOMErrorMessageContains(L"no space left on device");
654 + }
655 + }
656 +
657 + WSLC_TEST_METHOD(PushImage)
658 + {
659 + auto emptyAuth = BuildRegistryAuthHeader("", "");
660 +
661 + // Validate that pushing a non-existent image fails.
662 + {
663 + VERIFY_ARE_EQUAL(m_defaultSession->PushImage("does-not-exist:latest", emptyAuth.c_str(), nullptr), E_FAIL);
664 + ValidateCOMErrorMessage(L"An image does not exist locally with the tag: does-not-exist");
665 + }
666 +
667 + // Validate passing empty auth string returns an appropriate error.
668 + {
669 + VERIFY_ARE_EQUAL(m_defaultSession->PushImage("does-not-exist:latest", "", nullptr), E_INVALIDARG);
670 + }
671 +
672 + // Validate that PushImage() returns the appropriate error if the session is terminated.
673 + {
674 + VERIFY_SUCCEEDED(m_defaultSession->Terminate());
675 + auto cleanup = wil::scope_exit([&]() { ResetTestSession(); });
676 +
677 + VERIFY_ARE_EQUAL(m_defaultSession->PushImage("hello-world:latest", emptyAuth.c_str(), nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
678 + }
679 + }
680 +
681 + WSLC_TEST_METHOD(Authenticate)
682 + {
683 + constexpr auto c_username = "wslctest";
684 + constexpr auto c_password = "password";
685 +
686 + auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password);
687 +
688 + wil::unique_cotaskmem_ansistring token;
689 + VERIFY_ARE_EQUAL(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, "wrong-password", &token), E_FAIL);
690 + ValidateCOMErrorMessageContains(L"failed with status: 401 Unauthorized");
691 +
692 + VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, c_password, &token));
693 + VERIFY_IS_NOT_NULL(token.get());
694 +
695 + auto xRegistryAuth = BuildRegistryAuthHeader(c_username, c_password);
696 + auto image = PushImageToRegistry("hello-world:latest", registryAddress, xRegistryAuth);
697 +
698 + // Pulling without credentials should fail.
699 + VERIFY_ARE_EQUAL(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr), E_FAIL);
700 + ValidateCOMErrorMessageContains(L"no basic auth credentials");
701 +
702 + // Pulling with credentials should succeed.
703 + VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), xRegistryAuth.c_str(), nullptr));
704 + ExpectImagePresent(*m_defaultSession, image.c_str());
705 + }
706 +
707 + WSLC_TEST_METHOD(ListImages)
708 + {
709 + // Setup: Ensure debian:latest is available
710 + ExpectImagePresent(*m_defaultSession, "debian:latest");
711 +
712 + // Create additional tags for testing
713 + WSLCTagImageOptions tagOptions{};
714 + tagOptions.Image = "debian:latest";
715 + tagOptions.Repo = "debian";
716 + tagOptions.Tag = "test-tag1";
717 + VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
718 + tagOptions.Tag = "test-tag2";
719 + VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
720 +
721 + auto cleanup = wil::scope_exit([&]() {
722 + LOG_IF_FAILED(DeleteImageNoThrow("debian:test-tag1", WSLCDeleteImageFlagsNone).first);
723 + LOG_IF_FAILED(DeleteImageNoThrow("debian:test-tag2", WSLCDeleteImageFlagsNone).first);
724 + });
725 +
726 + LogInfo("Test: Basic listing with nullptr options");
727 + {
728 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
729 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
730 +
731 + VERIFY_IS_TRUE(images.size() > 0);
732 +
733 + // Find debian images and verify they exist
734 + bool foundLatest = false, foundTag1 = false, foundTag2 = false;
735 + for (const auto& image : images)
736 + {
737 + std::string imageName = image.Image;
738 + if (imageName == "debian:latest")
739 + {
740 + foundLatest = true;
741 + }
742 + if (imageName == "debian:test-tag1")
743 + {
744 + foundTag1 = true;
745 + }
746 + if (imageName == "debian:test-tag2")
747 + {
748 + foundTag2 = true;
749 + }
750 + }
751 +
752 + VERIFY_IS_TRUE(foundLatest);
753 + VERIFY_IS_TRUE(foundTag1);
754 + VERIFY_IS_TRUE(foundTag2);
755 + }
756 +
757 + LogInfo("Test: Verify all fields are populated");
758 + {
759 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
760 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
761 +
762 + std::string commonHash;
763 + int debianTagCount = 0;
764 +
765 + for (const auto& image : images)
766 + {
767 + std::string imageName = image.Image;
768 + if (imageName.starts_with("debian:"))
769 + {
770 + debianTagCount++;
771 +
772 + // Verify Hash field
773 + VERIFY_IS_TRUE(strlen(image.Hash) > 0);
774 + VERIFY_IS_TRUE(std::string(image.Hash).starts_with("sha256:"));
775 +
776 + // All debian tags should have the same hash (same underlying image)
777 + if (commonHash.empty())
778 + {
779 + commonHash = image.Hash;
780 + }
781 + else
782 + {
783 + VERIFY_ARE_EQUAL(commonHash, std::string(image.Hash));
784 + }
785 +
786 + // Verify Size field
787 + VERIFY_IS_TRUE(image.Size > 0);
788 +
789 + // Verify Created timestamp
790 + VERIFY_IS_TRUE(image.Created > 0);
791 + }
792 + }
793 +
794 + VERIFY_IS_TRUE(debianTagCount >= 3); // At least debian:latest, test-tag1, test-tag2
795 + }
796 +
797 + LogInfo("Test: Multiple tags for same image return separate entries");
798 + {
799 + WSLCListImageOptions options{};
800 + options.Flags = WSLCListImagesFlagsNone;
801 + options.Reference = "debian";
802 +
803 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
804 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
805 +
806 + // Should find at least our 3 debian tags
807 + VERIFY_IS_TRUE(images.size() >= 3);
808 +
809 + // Verify each tag is a separate entry
810 + std::set<std::string> imageTags;
811 + for (const auto& image : images)
812 + {
813 + imageTags.insert(image.Image);
814 + }
815 +
816 + VERIFY_IS_TRUE(imageTags.contains("debian:latest"));
817 + VERIFY_IS_TRUE(imageTags.contains("debian:test-tag1"));
818 + VERIFY_IS_TRUE(imageTags.contains("debian:test-tag2"));
819 + }
820 +
821 + LogInfo("Test: Filter by specific reference");
822 + {
823 + WSLCListImageOptions options{};
824 + options.Flags = WSLCListImagesFlagsNone;
825 + options.Reference = "debian:test-tag1";
826 +
827 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
828 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
829 +
830 + // When filtering by exact tag, Docker returns all tags for that image
831 + // So we should get debian:latest, debian:test-tag1, debian:test-tag2
832 + bool foundTag1 = false;
833 + for (const auto& image : images)
834 + {
835 + std::string imageName = image.Image;
836 + if (imageName == "debian:test-tag1")
837 + {
838 + foundTag1 = true;
839 + }
840 + }
841 + VERIFY_IS_TRUE(foundTag1);
842 + }
843 +
844 + LogInfo("Test: Digests flag");
845 + {
846 + WSLCListImageOptions options{};
847 + options.Flags = WSLCListImagesFlagsDigests;
848 + options.Reference = "debian:latest";
849 +
850 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
851 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
852 +
853 + // Check if digests are available (they may not be for all images)
854 + bool hasDigest = false;
855 + for (const auto& image : images)
856 + {
857 + if (strlen(image.Digest) > 0)
858 + {
859 + hasDigest = true;
860 + // Digest should be in format repo@sha256:...
861 + VERIFY_IS_TRUE(std::string(image.Digest).find("@sha256:") != std::string::npos);
862 + }
863 + }
864 + // Note: Pulled images from registry should have digests, locally built may not
865 + }
866 +
867 + LogInfo("Test: Before/Since filters");
868 + {
869 + // Get all images to find their IDs
870 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> allImages;
871 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, allImages.addressof(), allImages.size_address<ULONG>()));
872 +
873 + std::string debianId, pythonId;
874 + for (const auto& image : allImages)
875 + {
876 + std::string imageName = image.Image;
877 + if (imageName == "debian:latest")
878 + {
879 + debianId = image.Hash;
880 + }
881 + else if (imageName == "python:3.12-alpine")
882 + {
883 + pythonId = image.Hash;
884 + }
885 + }
886 +
887 + VERIFY_IS_FALSE(debianId.empty());
888 + VERIFY_IS_FALSE(pythonId.empty());
889 +
890 + // Test 'since' filter - images created after debian
891 + {
892 + WSLCListImageOptions options{};
893 + options.Flags = WSLCListImagesFlagsNone;
894 + options.Since = debianId.c_str();
895 +
896 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
897 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
898 + VERIFY_IS_TRUE(images.size() > 0);
899 +
900 + bool foundPython = false;
901 + for (const auto& image : images)
902 + {
903 + LogInfo("Image: %hs, Hash: %hs, Created: %lld", image.Image, image.Hash, image.Created);
904 + if (std::string{image.Image} == "python:3.12-alpine")
905 + {
906 + foundPython = true;
907 + }
908 + }
909 +
910 + VERIFY_IS_TRUE(foundPython);
911 + }
912 +
913 + // Test 'before' filter - images created before python
914 + {
915 + WSLCListImageOptions options{};
916 + options.Flags = WSLCListImagesFlagsNone;
917 + options.Before = pythonId.c_str();
918 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
919 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
920 + VERIFY_IS_TRUE(images.size() > 0);
921 +
922 + bool foundDebian = false;
923 + for (const auto& image : images)
924 + {
925 + if (std::string{image.Image} == "debian:latest")
926 + {
927 + foundDebian = true;
928 + }
929 + }
930 +
931 + VERIFY_IS_TRUE(foundDebian);
932 + }
933 + }
934 +
935 + LogInfo("Test: Dangling filter");
936 + {
937 + // Setup a dangling image
938 + LoadTestImage("alpine:latest");
939 + WSLCTagImageOptions tagOptions{};
940 + tagOptions.Image = "debian:latest";
941 + tagOptions.Repo = "alpine";
942 + tagOptions.Tag = "latest";
943 + VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
944 +
945 + auto alpineCleanup = wil::scope_exit([&]() {
946 + RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "image", "prune", "-f"});
947 + LOG_IF_FAILED(DeleteImageNoThrow("alpine:latest", WSLCDeleteImageFlagsNone).first);
948 + });
949 +
950 + // List only dangling images
951 + WSLCListImageOptions options{};
952 + options.Flags = WSLCListImagesFlagsDanglingTrue;
953 +
954 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> danglingImages;
955 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, danglingImages.addressof(), danglingImages.size_address<ULONG>()));
956 +
957 + VERIFY_ARE_EQUAL(1, danglingImages.size());
958 +
959 + // All dangling images should have <none>:<none> as the tag
960 + for (const auto& image : danglingImages)
961 + {
962 + std::string imageName = image.Image;
963 + VERIFY_ARE_EQUAL(imageName, std::string("<none>:<none>"));
964 + }
965 +
966 + // List non-dangling images
967 + options.Flags = WSLCListImagesFlagsDanglingFalse;
968 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> nonDanglingImages;
969 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, nonDanglingImages.addressof(), nonDanglingImages.size_address<ULONG>()));
970 + VERIFY_IS_TRUE(nonDanglingImages.size() > 0);
971 +
972 + // None of these should be <none>:<none>
973 + for (const auto& image : nonDanglingImages)
974 + {
975 + std::string imageName = image.Image;
976 + VERIFY_ARE_NOT_EQUAL(imageName, std::string("<none>:<none>"));
977 + }
978 + }
979 +
980 + LogInfo("Test: Label filter");
981 + {
982 + // Test with nullptr (no label filter)
983 + WSLCListImageOptions options{};
984 + options.Flags = WSLCListImagesFlagsNone;
985 + options.Labels = nullptr;
986 + options.LabelsCount = 0;
987 +
988 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
989 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
990 +
991 + // Test with single label filter
992 + {
993 + WSLCLabel labels[] = {{.Key = "test.label", .Value = nullptr}};
994 + options.Labels = labels;
995 + options.LabelsCount = 1;
996 +
997 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
998 + }
999 +
1000 + // Test with multiple label filters (labels are AND'ed together)
1001 + {
1002 + WSLCLabel labels[] = {{.Key = "test.label1", .Value = nullptr}, {.Key = "test.label2", .Value = "value"}};
1003 + options.Labels = labels;
1004 + options.LabelsCount = 2;
1005 +
1006 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
1007 + }
1008 +
1009 + // Note: To fully test label filtering with actual matches, would need to:
1010 + // 1. Build an image with specific labels using docker build --label
1011 + // 2. Filter with matching labels
1012 + // 3. Verify the filtered image appears
1013 + // This only tests the API usage not fail without requiring image builds
1014 + }
1015 +
1016 + cleanup.reset();
1017 + ExpectImagePresent(*m_defaultSession, "debian:test-tag1", false);
1018 + ExpectImagePresent(*m_defaultSession, "debian:test-tag2", false);
1019 + ExpectImagePresent(*m_defaultSession, "debian:latest", true);
1020 + }
1021 +
1022 + WSLC_TEST_METHOD(LoadImage)
1023 + {
1024 + // This test case is hanging on Windows Server SKUs. Skip the test until the issue is resolved.
1025 + // TODO: Remove once the fix is available.
1026 + if (IsWindowsServer())
1027 + {
1028 + SKIP_TEST_UNSTABLE();
1029 + }
1030 +
1031 + std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
1032 + wil::unique_handle imageTarFileHandle{
1033 + CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
1034 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
1035 +
1036 + LARGE_INTEGER fileSize{};
1037 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
1038 +
1039 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
1040 +
1041 + // Verify that the image is in the list of images.
1042 + ExpectImagePresent(*m_defaultSession, "hello-world:latest");
1043 +
1044 + // Validate container launch from the loaded image
1045 + {
1046 + WSLCContainerLauncher launcher("hello-world:latest", "wslc-load-image-container");
1047 +
1048 + auto container = launcher.Launch(*m_defaultSession);
1049 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
1050 +
1051 + VERIFY_ARE_EQUAL(0, result.Code);
1052 + VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
1053 + }
1054 +
1055 + // Validate that invalid tars fail with proper error message and code.
1056 + {
1057 + auto currentExecutableHandle = wil::open_file(wil::GetModuleFileNameW<std::wstring>().c_str());
1058 + VERIFY_IS_TRUE(GetFileSizeEx(currentExecutableHandle.get(), &fileSize));
1059 +
1060 + VERIFY_ARE_EQUAL(m_defaultSession->LoadImage(ToCOMInputHandle(currentExecutableHandle.get()), nullptr, fileSize.QuadPart), E_FAIL);
1061 +
1062 + ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
1063 + }
1064 +
1065 + // Validate that LoadImage fails when the input pipe is closed during reading.
1066 + {
1067 + wil::unique_handle pipeRead;
1068 + wil::unique_handle pipeWrite;
1069 + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
1070 +
1071 + std::promise<HRESULT> loadResult;
1072 + std::thread operationThread([&]() {
1073 + loadResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024));
1074 + });
1075 +
1076 + auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
1077 +
1078 + // Write some data to ensure the service has started reading from the pipe (pipe buffer is 2 bytes).
1079 + DWORD bytesWritten{};
1080 + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
1081 +
1082 + // Close the write end.
1083 + pipeWrite.reset();
1084 +
1085 + VERIFY_ARE_EQUAL(E_FAIL, loadResult.get_future().get());
1086 + }
1087 +
1088 + // Validate that LoadImage is aborted when the session terminates.
1089 + {
1090 + wil::unique_handle pipeRead;
1091 + wil::unique_handle pipeWrite;
1092 + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
1093 +
1094 + std::promise<HRESULT> terminateResult;
1095 + wil::unique_event testCompleted{wil::EventOptions::ManualReset};
1096 + std::thread operationThread([&]() {
1097 + terminateResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024));
1098 + WI_ASSERT(testCompleted.is_signaled());
1099 + });
1100 +
1101 + auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
1102 +
1103 + // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes).
1104 + DWORD bytesWritten{};
1105 + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
1106 +
1107 + testCompleted.SetEvent();
1108 +
1109 + VERIFY_SUCCEEDED(m_defaultSession->Terminate());
1110 +
1111 + auto restore = ResetTestSession();
1112 +
1113 + auto hr = terminateResult.get_future().get();
1114 + VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED));
1115 + }
1116 + }
1117 +
1118 + WSLC_TEST_METHOD(ImportImage)
1119 + {
1120 + auto cleanup =
1121 + wil::scope_exit([&]() { LOG_IF_FAILED(DeleteImageNoThrow("my-hello-world:test", WSLCDeleteImageFlagsNone).first); });
1122 +
1123 + std::filesystem::path imageTar = std::filesystem::path{g_testDataPath} / L"HelloWorldExported.tar";
1124 + wil::unique_handle imageTarFileHandle{
1125 + CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
1126 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
1127 +
1128 + LARGE_INTEGER fileSize{};
1129 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
1130 +
1131 + VERIFY_SUCCEEDED(m_defaultSession->ImportImage(
1132 + ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world:test", nullptr, fileSize.QuadPart));
1133 +
1134 + ExpectImagePresent(*m_defaultSession, "my-hello-world:test");
1135 +
1136 + // Validate that containers can be started from the imported image.
1137 + {
1138 + WSLCContainerLauncher launcher("my-hello-world:test", "wslc-import-image-container", {"/hello"});
1139 +
1140 + auto container = launcher.Launch(*m_defaultSession);
1141 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
1142 +
1143 + VERIFY_ARE_EQUAL(0, result.Code);
1144 + VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
1145 + }
1146 +
1147 + // Validate that ImportImage fails if no tag is passed
1148 + {
1149 + VERIFY_ARE_EQUAL(
1150 + m_defaultSession->ImportImage(ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world", nullptr, fileSize.QuadPart),
1151 + E_INVALIDARG);
1152 + }
1153 +
1154 + // Validate that invalid tars fail with proper error message and code.
1155 + {
1156 + auto currentExecutableHandle = wil::open_file(wil::GetModuleFileNameW<std::wstring>().c_str());
1157 +
1158 + VERIFY_IS_TRUE(GetFileSizeEx(currentExecutableHandle.get(), &fileSize));
1159 +
1160 + VERIFY_ARE_EQUAL(
1161 + m_defaultSession->ImportImage(
1162 + ToCOMInputHandle(currentExecutableHandle.get()), "invalid-image:test", nullptr, fileSize.QuadPart),
1163 + E_FAIL);
1164 +
1165 + ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
1166 + }
1167 +
1168 + // Validate that ImportImage fails when the input pipe is closed during reading.
1169 + {
1170 + wil::unique_handle pipeRead;
1171 + wil::unique_handle pipeWrite;
1172 + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
1173 +
1174 + std::promise<HRESULT> importResult;
1175 + std::thread operationThread([&]() {
1176 + importResult.set_value(m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "broken-read:eof", nullptr, 1024 * 1024));
1177 + });
1178 +
1179 + auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
1180 +
1181 + // Write some data to ensure the service has started reading from the pipe (pipe buffer is 2 bytes).
1182 + DWORD bytesWritten{};
1183 + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
1184 +
1185 + // Close the write end.
1186 + pipeWrite.reset();
1187 +
1188 + VERIFY_ARE_EQUAL(E_FAIL, importResult.get_future().get());
1189 + }
1190 +
1191 + // Validate that ImportImage is aborted when the session terminates.
1192 + {
1193 + wil::unique_handle pipeRead;
1194 + wil::unique_handle pipeWrite;
1195 + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
1196 +
1197 + std::promise<HRESULT> terminateResult;
1198 + wil::unique_event testCompleted{wil::EventOptions::ManualReset};
1199 + std::thread operationThread([&]() {
1200 + terminateResult.set_value(
1201 + m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "session-terminate:test", nullptr, 1024 * 1024));
1202 + WI_ASSERT(testCompleted.is_signaled());
1203 + });
1204 +
1205 + auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
1206 +
1207 + // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes).
1208 + DWORD bytesWritten{};
1209 + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
1210 +
1211 + testCompleted.SetEvent();
1212 +
1213 + VERIFY_SUCCEEDED(m_defaultSession->Terminate());
1214 +
1215 + auto restore = ResetTestSession();
1216 +
1217 + auto hr = terminateResult.get_future().get();
1218 + VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED));
1219 + }
1220 + }
1221 +
1222 + WSLC_TEST_METHOD(DeleteImage)
1223 + {
1224 + // Prepare alpine image to delete.
1225 + LoadTestImage("alpine:latest");
1226 +
1227 + // Verify that the image is in the list of images.
1228 + ExpectImagePresent(*m_defaultSession, "alpine:latest");
1229 +
1230 + // Launch a container to ensure that image deletion fails when in use.
1231 + WSLCContainerLauncher launcher(
1232 + "alpine:latest", "test-delete-container-in-use", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost);
1233 +
1234 + auto container = launcher.Launch(*m_defaultSession);
1235 +
1236 + // Verify that the container is in running state.
1237 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
1238 +
1239 + // Test delete failed if image in use.
1240 + VERIFY_ARE_EQUAL(
1241 + HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), DeleteImageNoThrow("alpine:latest", WSLCDeleteImageFlagsNone).first);
1242 +
1243 + // Force should succeed.
1244 + auto deletedImages = DeleteImage("alpine:latest", WSLCDeleteImageFlagsForce);
1245 + VERIFY_IS_TRUE(deletedImages.size() > 0);
1246 + VERIFY_IS_TRUE(std::strlen(deletedImages[0].Image) > 0);
1247 +
1248 + // Verify that the image is no longer in the list of images.
1249 + ExpectImagePresent(*m_defaultSession, "alpine:latest", false);
1250 +
1251 + // Test delete failed if image does not exist.
1252 + VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, DeleteImageNoThrow("alpine:latest", WSLCDeleteImageFlagsForce).first);
1253 +
1254 + // Validate that invalid flags are rejected.
1255 + {
1256 + WSLCDeleteImageOptions invalidOptions{.Image = "alpine:latest", .Flags = 0x4};
1257 + VERIFY_ARE_EQUAL(
1258 + m_defaultSession->DeleteImage(&invalidOptions, deletedImages.addressof(), deletedImages.size_address<ULONG>()), E_INVALIDARG);
1259 + }
1260 + }
1261 +
1262 + void ValidateCOMErrorMessage(const std::optional<std::wstring>& Expected, const std::source_location& Source = std::source_location::current())
1263 + {
1264 + auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
1265 +
1266 + if (comError.has_value())
1267 + {
1268 + if (!Expected.has_value())
1269 + {
1270 + LogError("Unexpected COM error: '%ls'. Source: %hs", comError->Message.get(), std::format("{}", Source).c_str());
1271 + VERIFY_FAIL();
1272 + }
1273 +
1274 + VERIFY_ARE_EQUAL(Expected.value(), comError->Message.get());
1275 + }
1276 + else
1277 + {
1278 + if (Expected.has_value())
1279 + {
1280 + LogError("Expected COM error: '%ls' but none was set. Source: %hs", Expected->c_str(), std::format("{}", Source).c_str());
1281 + VERIFY_FAIL();
1282 + }
1283 + }
1284 + }
1285 +
1286 + void ValidateCOMErrorMessageContains(const std::wstring& ExpectedSubstring)
1287 + {
1288 + auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
1289 +
1290 + if (comError.has_value())
1291 + {
1292 + if (!comError->Message)
1293 + {
1294 + LogError("Expected COM error containing: '%ls', but COM error message was null", ExpectedSubstring.c_str());
1295 + VERIFY_FAIL();
1296 + }
1297 +
1298 + if (wcsstr(comError->Message.get(), ExpectedSubstring.c_str()) == nullptr)
1299 + {
1300 + LogError("Expected COM error containing: '%ls', but got: '%ls'", ExpectedSubstring.c_str(), comError->Message.get());
1301 + VERIFY_FAIL();
1302 + }
1303 + }
1304 + else
1305 + {
1306 + LogError("Expected COM error containing: '%ls' but none was set", ExpectedSubstring.c_str());
1307 + VERIFY_FAIL();
1308 + }
1309 + }
1310 +
1311 + class CapturingProgressCallback
1312 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
1313 + {
1314 + public:
1315 + CapturingProgressCallback(std::string& output) : m_output(output)
1316 + {
1317 + }
1318 +
1319 + HRESULT OnProgress(LPCSTR status, LPCSTR, ULONGLONG, ULONGLONG) override
1320 + {
1321 + m_output.append(status);
1322 + return S_OK;
1323 + }
1324 +
1325 + private:
1326 + std::string& m_output;
1327 + };
1328 +
1329 + HRESULT BuildImageFromContext(const std::filesystem::path& contextDir, const WSLCBuildImageOptions* options, IProgressCallback* callback = nullptr)
1330 + {
1331 + auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
1332 +
1333 + auto contextPathStr = contextDir.wstring();
1334 + WSLCBuildImageOptions optionsCopy = *options;
1335 + optionsCopy.ContextPath = contextPathStr.c_str();
1336 + optionsCopy.DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get());
1337 +
1338 + auto buildResult = m_defaultSession->BuildImage(&optionsCopy, callback, nullptr);
1339 +
1340 + if (FAILED(buildResult))
1341 + {
1342 + LogInfo("BuildImage failed: 0x%08x", buildResult);
1343 + }
1344 +
1345 + return buildResult;
1346 + }
1347 +
1348 + HRESULT BuildImageFromContext(const std::filesystem::path& contextDir, const char* imageTag)
1349 + {
1350 + LPCSTR tag = imageTag;
1351 + WSLCBuildImageOptions options{
1352 + .Tags = {&tag, 1},
1353 + };
1354 + return BuildImageFromContext(contextDir, &options);
1355 + }
1356 +
1357 + WSLC_TEST_METHOD(BuildImage)
1358 + {
1359 + auto contextDir = std::filesystem::current_path() / "build-context";
1360 + std::filesystem::create_directories(contextDir);
1361 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1362 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build:latest", WSLCDeleteImageFlagsForce).first);
1363 +
1364 + std::error_code ec;
1365 + std::filesystem::remove_all(contextDir, ec);
1366 + });
1367 +
1368 + {
1369 + std::ofstream dockerfile(contextDir / "Dockerfile");
1370 + dockerfile << "FROM debian:latest\n";
1371 + dockerfile << "CMD [\"echo\", \"Hello from a WSL container!\"]\n";
1372 + }
1373 +
1374 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build:latest"));
1375 + ExpectImagePresent(*m_defaultSession, "wslc-test-build:latest");
1376 +
1377 + WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-build-test-container");
1378 + auto container = launcher.Launch(*m_defaultSession);
1379 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
1380 +
1381 + VERIFY_ARE_EQUAL(0, result.Code);
1382 + VERIFY_IS_TRUE(result.Output[1].find("Hello from a WSL container!") != std::string::npos);
1383 + }
1384 +
1385 + // This test validates both that we can build an image with an empty CMD, and that we can run such an image.
1386 + WSLC_TEST_METHOD(BuildImageEntrypoint)
1387 + {
1388 + auto contextDir = std::filesystem::current_path() / "build-context-entrypoint";
1389 + std::filesystem::create_directories(contextDir);
1390 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1391 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-entrypoint:latest", WSLCDeleteImageFlagsForce).first);
1392 +
1393 + std::error_code ec;
1394 + std::filesystem::remove_all(contextDir, ec);
1395 + });
1396 +
1397 + {
1398 + std::ofstream dockerfile(contextDir / "Dockerfile");
1399 + dockerfile << "FROM debian:latest\n";
1400 + dockerfile << "CMD []\n";
1401 + dockerfile << "ENTRYPOINT [\"/bin/echo\", \"Entrypoint\"]\n";
1402 + }
1403 +
1404 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-entrypoint:latest"));
1405 + ExpectImagePresent(*m_defaultSession, "wslc-test-entrypoint:latest");
1406 +
1407 + // Validate that the entrypoint is started by default.
1408 + {
1409 + WSLCContainerLauncher launcher("wslc-test-entrypoint:latest", "wslc-entrypoint-test-1");
1410 + auto container = launcher.Launch(*m_defaultSession);
1411 + auto initProcess = container.GetInitProcess();
1412 + ValidateProcessOutput(initProcess, {{1, "Entrypoint\n"}});
1413 + }
1414 +
1415 + // Validate that arguments are passed to the entrypoint, and don't override it.
1416 + {
1417 + WSLCContainerLauncher launcher("wslc-test-entrypoint:latest", "wslc-entrypoint-test-2", {"extra-arg"});
1418 + auto container = launcher.Launch(*m_defaultSession);
1419 + auto initProcess = container.GetInitProcess();
1420 + ValidateProcessOutput(initProcess, {{1, "Entrypoint extra-arg\n"}});
1421 + }
1422 +
1423 + // Validate that the entrypoint can be overridden.
1424 + {
1425 + WSLCContainerLauncher launcher("wslc-test-entrypoint:latest", "wslc-entrypoint-test-3");
1426 + launcher.SetEntrypoint({"/bin/echo", "OverriddenEntrypoint"});
1427 + auto container = launcher.Launch(*m_defaultSession);
1428 + auto initProcess = container.GetInitProcess();
1429 + ValidateProcessOutput(initProcess, {{1, "OverriddenEntrypoint\n"}});
1430 + }
1431 +
1432 + // Validate that the entrypoint can be overridden and that CMD args are passed to the entrypoint.
1433 + {
1434 + WSLCContainerLauncher launcher("wslc-test-entrypoint:latest", "wslc-entrypoint-test-4", {"extra-arg"});
1435 + launcher.SetEntrypoint({"/bin/echo", "OverriddenEntrypoint"});
1436 + auto container = launcher.Launch(*m_defaultSession);
1437 + auto initProcess = container.GetInitProcess();
1438 + ValidateProcessOutput(initProcess, {{1, "OverriddenEntrypoint extra-arg\n"}});
1439 + }
1440 + }
1441 +
1442 + WSLC_TEST_METHOD(BuildImageWithContext)
1443 + {
1444 + auto contextDir = std::filesystem::current_path() / "build-context-file";
1445 + std::filesystem::create_directories(contextDir);
1446 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1447 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-context:latest", WSLCDeleteImageFlagsForce).first);
1448 +
1449 + std::error_code ec;
1450 + std::filesystem::remove_all(contextDir, ec);
1451 + });
1452 +
1453 + {
1454 + std::ofstream dockerfile(contextDir / "Dockerfile");
1455 + dockerfile << "FROM debian:latest\n";
1456 + dockerfile << "COPY message.txt /message.txt\n";
1457 + dockerfile << "CMD [\"cat\", \"/message.txt\"]\n";
1458 + }
1459 +
1460 + {
1461 + std::ofstream message(contextDir / "message.txt");
1462 + message << "Hello from a WSL container context file!\n";
1463 + }
1464 +
1465 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-context:latest"));
1466 + ExpectImagePresent(*m_defaultSession, "wslc-test-build-context:latest");
1467 +
1468 + WSLCContainerLauncher launcher("wslc-test-build-context:latest", "wslc-build-context-container");
1469 + auto container = launcher.Launch(*m_defaultSession);
1470 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
1471 +
1472 + VERIFY_ARE_EQUAL(0, result.Code);
1473 + VERIFY_IS_TRUE(result.Output[1].find("Hello from a WSL container context file!") != std::string::npos);
1474 + }
1475 +
1476 + WSLC_TEST_METHOD(BuildImageManyFiles)
1477 + {
1478 + static constexpr int fileCount = 1024;
1479 +
1480 + auto contextDir = std::filesystem::current_path() / "build-context-many";
1481 + std::filesystem::create_directories(contextDir / "files");
1482 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1483 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-many:latest", WSLCDeleteImageFlagsForce).first);
1484 +
1485 + std::error_code ec;
1486 + std::filesystem::remove_all(contextDir, ec);
1487 + });
1488 +
1489 + // Generate the context files.
1490 + for (int i = 0; i < fileCount; i++)
1491 + {
1492 + auto name = std::format("file{:04d}.txt", i);
1493 + auto content = std::format("content-{:04d}\n", i);
1494 + std::ofstream file(contextDir / "files" / name);
1495 + file << content;
1496 + }
1497 +
1498 + {
1499 + std::ofstream dockerfile(contextDir / "Dockerfile");
1500 + dockerfile << "FROM debian:latest\n";
1501 + dockerfile << "COPY files/ /files/\n";
1502 + // Verify every file is present and contains the expected content.
1503 + // Only mismatches are printed; on success just the sentinel.
1504 + dockerfile << "CMD [\"sh\", \"-c\", "
1505 + << "\"cd /files && failed=0 && "
1506 + << "for i in $(seq 0 " << (fileCount - 1) << "); do "
1507 + << "f=$(printf 'file%04d.txt' $i); "
1508 + << "e=$(printf 'content-%04d' $i); "
1509 + << "if [ ! -f $f ]; then echo MISSING:$f; failed=1; "
1510 + << "elif ! grep -q $e $f; then echo BAD:$f; failed=1; fi; "
1511 + << "done && "
1512 + << "[ $failed -eq 0 ] && echo all_ok_" << fileCount << "\"]\n";
1513 + }
1514 +
1515 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-many:latest"));
1516 + ExpectImagePresent(*m_defaultSession, "wslc-test-build-many:latest");
1517 +
1518 + WSLCContainerLauncher launcher("wslc-test-build-many:latest", "wslc-build-many-container");
1519 + auto container = launcher.Launch(*m_defaultSession);
1520 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
1521 +
1522 + VERIFY_ARE_EQUAL(0, result.Code);
1523 + auto sentinel = std::format("all_ok_{}", fileCount);
1524 + VERIFY_IS_TRUE(result.Output[1].find(sentinel) != std::string::npos);
1525 + }
1526 +
1527 + WSLC_TEST_METHOD(BuildImageLargeFile)
1528 + {
1529 + RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "rmi", "-f", "wslc-test-build-large:latest"});
1530 + ExpectCommandResult(m_defaultSession.get(), {"/usr/bin/docker", "builder", "prune", "-f"}, 0);
1531 +
1532 + auto contextDir = std::filesystem::current_path() / "build-context-large";
1533 + std::filesystem::create_directories(contextDir);
1534 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1535 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-large:latest", WSLCDeleteImageFlagsForce).first);
1536 +
1537 + std::error_code ec;
1538 + std::filesystem::remove_all(contextDir, ec);
1539 + });
1540 +
1541 + static constexpr int fileSizeMb = 1024;
1542 +
1543 + {
1544 + std::ofstream dockerfile(contextDir / "Dockerfile");
1545 + dockerfile << "FROM debian:latest\n";
1546 + dockerfile << "COPY large.bin /large.bin\n";
1547 + dockerfile << std::format(
1548 + "CMD [\"sh\", \"-c\", \"test $(stat -c %s /large.bin) -eq {} && echo size_ok\"]\n",
1549 + static_cast<long long>(fileSizeMb) * 1024 * 1024);
1550 + }
1551 +
1552 + {
1553 + auto largePath = contextDir / "large.bin";
1554 + wil::unique_hfile largeFile{CreateFileW(largePath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
1555 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == largeFile.get());
1556 +
1557 + std::vector<char> buffer(1024 * 1024, '\0');
1558 + for (int i = 0; i < fileSizeMb; i++)
1559 + {
1560 + DWORD written = 0;
1561 + if (!WriteFile(largeFile.get(), buffer.data(), static_cast<DWORD>(buffer.size()), &written, nullptr) ||
1562 + written != static_cast<DWORD>(buffer.size()))
1563 + {
1564 + LogError("WriteFile failed at chunk %d/%d: 0x%08x", i, fileSizeMb, GetLastError());
1565 + VERIFY_FAIL();
1566 + }
1567 + }
1568 + }
1569 +
1570 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-large:latest"));
1571 + ExpectImagePresent(*m_defaultSession, "wslc-test-build-large:latest");
1572 +
1573 + WSLCContainerLauncher launcher("wslc-test-build-large:latest", "wslc-build-large-container");
1574 + auto container = launcher.Launch(*m_defaultSession);
1575 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
1576 +
1577 + VERIFY_ARE_EQUAL(0, result.Code);
1578 + VERIFY_IS_TRUE(result.Output[1].find("size_ok") != std::string::npos);
1579 + }
1580 +
1581 + WSLC_TEST_METHOD(BuildImageMultiStage)
1582 + {
1583 + auto contextDir = std::filesystem::current_path() / "build-context-multistage";
1584 + std::filesystem::create_directories(contextDir);
1585 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1586 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-multistage:latest", WSLCDeleteImageFlagsForce).first);
1587 +
1588 + std::error_code ec;
1589 + std::filesystem::remove_all(contextDir, ec);
1590 + });
1591 +
1592 + {
1593 + std::ofstream dockerfile(contextDir / "Dockerfile");
1594 + // Two independent stages that can build in parallel, each producing
1595 + // part of the final output. The last stage combines them.
1596 + dockerfile << "FROM debian:latest AS greeting\n";
1597 + dockerfile << "RUN echo -n 'WSL containers' | tee /part.txt\n";
1598 + dockerfile << "\n";
1599 + dockerfile << "FROM debian:latest AS description\n";
1600 + dockerfile << "RUN echo -n 'support multi-stage builds' | tee /part.txt\n";
1601 + dockerfile << "\n";
1602 + dockerfile << "FROM debian:latest\n";
1603 + dockerfile << "COPY --from=greeting /part.txt /greeting.txt\n";
1604 + dockerfile << "COPY --from=description /part.txt /description.txt\n";
1605 + dockerfile << "CMD [\"sh\", \"-c\", "
1606 + << "\"echo \\\"$(cat /greeting.txt) $(cat /description.txt)\\\"\"]\n";
1607 + }
1608 +
1609 + std::string output;
1610 + auto callback = Microsoft::WRL::Make<CapturingProgressCallback>(output);
1611 + LPCSTR tag = "wslc-test-build-multistage:latest";
1612 + WSLCBuildImageOptions options{.Tags = {&tag, 1}, .Flags = WSLCBuildImageFlagsNoCache};
1613 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options, callback.Get()));
1614 + VERIFY_IS_TRUE(output.find("[greeting] WSL containers") != std::string::npos);
1615 + VERIFY_IS_TRUE(output.find("[description] support multi-stage builds") != std::string::npos);
1616 + ExpectImagePresent(*m_defaultSession, "wslc-test-build-multistage:latest");
1617 +
1618 + WSLCContainerLauncher launcher("wslc-test-build-multistage:latest", "wslc-build-multistage-container");
1619 + auto container = launcher.Launch(*m_defaultSession);
1620 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
1621 +
1622 + VERIFY_ARE_EQUAL(0, result.Code);
1623 + VERIFY_IS_TRUE(result.Output[1].find("WSL containers support multi-stage builds") != std::string::npos);
1624 + }
1625 +
1626 + WSLC_TEST_METHOD(BuildImageDockerIgnore)
1627 + {
1628 + auto contextDir = std::filesystem::current_path() / "build-context-dockerignore";
1629 + std::filesystem::create_directories(contextDir / "temp");
1630 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1631 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-dockerignore:latest", WSLCDeleteImageFlagsForce).first);
1632 +
1633 + std::error_code ec;
1634 + std::filesystem::remove_all(contextDir, ec);
1635 + });
1636 +
1637 + {
1638 + std::ofstream ignore(contextDir / ".dockerignore");
1639 + ignore << "# Ignore log files and temp directory\n";
1640 + ignore << "*.log\n";
1641 + ignore << "temp/\n";
1642 + }
1643 +
1644 + {
1645 + std::ofstream(contextDir / "keep.txt") << "kept\n";
1646 + std::ofstream(contextDir / "debug.log") << "excluded\n";
1647 + std::ofstream(contextDir / "temp" / "cache.dat") << "excluded\n";
1648 + }
1649 +
1650 + {
1651 + std::ofstream dockerfile(contextDir / "Dockerfile");
1652 + dockerfile << "FROM debian:latest\n";
1653 + dockerfile << "COPY . /ctx/\n";
1654 + dockerfile << "CMD [\"sh\", \"-c\", "
1655 + << "\"test -f /ctx/keep.txt "
1656 + << "&& ! test -f /ctx/debug.log "
1657 + << "&& ! test -d /ctx/temp "
1658 + << "&& echo dockerignore_ok\"]\n";
1659 + }
1660 +
1661 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-dockerignore:latest"));
1662 + ExpectImagePresent(*m_defaultSession, "wslc-test-build-dockerignore:latest");
1663 +
1664 + WSLCContainerLauncher launcher("wslc-test-build-dockerignore:latest", "wslc-build-dockerignore-container");
1665 + auto container = launcher.Launch(*m_defaultSession);
1666 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
1667 +
1668 + VERIFY_ARE_EQUAL(0, result.Code);
1669 + VERIFY_IS_TRUE(result.Output[1].find("dockerignore_ok") != std::string::npos);
1670 + }
1671 +
1672 + WSLC_TEST_METHOD(BuildImageFailure)
1673 + {
1674 + auto contextDir = std::filesystem::current_path() / "build-context-failure";
1675 + std::filesystem::create_directories(contextDir);
1676 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1677 + std::error_code ec;
1678 + std::filesystem::remove_all(contextDir, ec);
1679 + });
1680 +
1681 + {
1682 + std::ofstream dockerfile(contextDir / "Dockerfile");
1683 + dockerfile << "FROM does-not-exist:invalid\n";
1684 + }
1685 +
1686 + VERIFY_FAILED(BuildImageFromContext(contextDir, "wslc-test-build-failure:latest"));
1687 + auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
1688 + VERIFY_IS_TRUE(comError.has_value());
1689 + LogInfo("Expected build error: %ls", comError->Message.get());
1690 +
1691 + ExpectImagePresent(*m_defaultSession, "wslc-test-build-failure:latest", false);
1692 + }
1693 +
1694 + WSLC_TEST_METHOD(BuildImageFailureShowsBuildOutput)
1695 + {
1696 + auto contextDir = std::filesystem::current_path() / "build-context-failure-output";
1697 + std::filesystem::create_directories(contextDir);
1698 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1699 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-args:latest", WSLCDeleteImageFlagsForce).first);
1700 +
1701 + std::error_code ec;
1702 + std::filesystem::remove_all(contextDir, ec);
1703 + });
1704 +
1705 + {
1706 + std::ofstream dockerfile(contextDir / "Dockerfile");
1707 + dockerfile << "FROM debian:latest\n";
1708 + dockerfile << "RUN echo 'build-log-marker' && /bin/false\n";
1709 + }
1710 +
1711 + class ProgressAccumulator
1712 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
1713 + {
1714 + public:
1715 + ProgressAccumulator(std::string& output) : m_output(output)
1716 + {
1717 + }
1718 + HRESULT OnProgress(LPCSTR message, LPCSTR, ULONGLONG, ULONGLONG) override
1719 + {
1720 + if (message)
1721 + {
1722 + m_output.append(message);
1723 + }
1724 + return S_OK;
1725 + }
1726 +
1727 + private:
1728 + std::string& m_output;
1729 + };
1730 +
1731 + std::string progressOutput;
1732 + auto callback = Microsoft::WRL::Make<ProgressAccumulator>(progressOutput);
1733 +
1734 + auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
1735 + auto contextPathStr = contextDir.wstring();
1736 + LPCSTR tag = "wslc-test-build-failure-output:latest";
1737 + WSLCBuildImageOptions options{
1738 + .ContextPath = contextPathStr.c_str(),
1739 + .DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get()),
1740 + .Tags = {&tag, 1},
1741 + };
1742 +
1743 + VERIFY_FAILED(m_defaultSession->BuildImage(&options, callback.Get(), nullptr));
1744 + VERIFY_IS_TRUE(progressOutput.find("build-log-marker") != std::string::npos);
1745 + }
1746 +
1747 + WSLC_TEST_METHOD(BuildImageStdinDockerfile)
1748 + {
1749 + auto contextDir = std::filesystem::current_path() / "build-context-stdin";
1750 + std::filesystem::create_directories(contextDir);
1751 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1752 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-stdin:latest", WSLCDeleteImageFlagsForce).first);
1753 +
1754 + std::error_code ec;
1755 + std::filesystem::remove_all(contextDir, ec);
1756 + });
1757 +
1758 + auto dockerfileContent = "FROM debian:latest\nCMD [\"echo\", \"stdin-dockerfile-ok\"]\n";
1759 +
1760 + wil::unique_hfile readHandle;
1761 + wil::unique_hfile writeHandle;
1762 + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(readHandle.addressof(), writeHandle.addressof(), nullptr, 0));
1763 +
1764 + DWORD bytesWritten;
1765 + THROW_IF_WIN32_BOOL_FALSE(
1766 + WriteFile(writeHandle.get(), dockerfileContent, static_cast<DWORD>(strlen(dockerfileContent)), &bytesWritten, nullptr));
1767 + writeHandle.reset();
1768 +
1769 + auto contextPathStr = contextDir.wstring();
1770 + LPCSTR tag = "wslc-test-build-stdin:latest";
1771 + WSLCBuildImageOptions options{
1772 + .ContextPath = contextPathStr.c_str(),
1773 + .DockerfileHandle = ToCOMInputHandle(readHandle.get()),
1774 + .Tags = {&tag, 1},
1775 + };
1776 + VERIFY_SUCCEEDED(m_defaultSession->BuildImage(&options, nullptr, nullptr));
1777 + ExpectImagePresent(*m_defaultSession, "wslc-test-build-stdin:latest");
1778 +
1779 + WSLCContainerLauncher launcher("wslc-test-build-stdin:latest", "wslc-build-stdin-container");
1780 + auto container = launcher.Launch(*m_defaultSession);
1781 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
1782 +
1783 + VERIFY_ARE_EQUAL(0, result.Code);
1784 + VERIFY_IS_TRUE(result.Output[1].find("stdin-dockerfile-ok") != std::string::npos);
1785 + }
1786 +
1787 + WSLC_TEST_METHOD(BuildImageBuildArgs)
1788 + {
1789 + auto contextDir = std::filesystem::current_path() / "build-context-buildargs";
1790 + std::filesystem::create_directories(contextDir);
1791 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1792 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-args:latest", WSLCDeleteImageFlagsForce).first);
1793 +
1794 + std::error_code ec;
1795 + std::filesystem::remove_all(contextDir, ec);
1796 + });
1797 +
1798 + {
1799 + std::ofstream dockerfile(contextDir / "Dockerfile");
1800 + dockerfile << "FROM debian:latest\n";
1801 + dockerfile << "ARG TEST_VALUE\n";
1802 + dockerfile << "ENV TEST_VALUE=${TEST_VALUE}\n";
1803 + dockerfile << "CMD echo \"build-arg-value=${TEST_VALUE}\"\n";
1804 + }
1805 +
1806 + LPCSTR tag = "wslc-test-build-args:latest";
1807 + LPCSTR buildArg = "TEST_VALUE=hello-from-build-arg";
1808 + WSLCBuildImageOptions options{.Tags = {&tag, 1}, .BuildArgs = {&buildArg, 1}};
1809 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options));
1810 + ExpectImagePresent(*m_defaultSession, "wslc-test-build-args:latest");
1811 +
1812 + WSLCContainerLauncher launcher("wslc-test-build-args:latest", "wslc-build-args-container");
1813 + auto container = launcher.Launch(*m_defaultSession);
1814 + auto initProcess = container.GetInitProcess();
1815 + ValidateProcessOutput(initProcess, {{1, "build-arg-value=hello-from-build-arg\n"}});
1816 + }
1817 +
1818 + WSLC_TEST_METHOD(BuildImageMultipleTags)
1819 + {
1820 + auto contextDir = std::filesystem::current_path() / "build-context-multitag";
1821 + std::filesystem::create_directories(contextDir);
1822 + LPCSTR tags[] = {"wslc-test-multitag:v1", "wslc-test-multitag:v2"};
1823 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1824 + for (auto* tag : tags)
1825 + {
1826 + LOG_IF_FAILED(DeleteImageNoThrow(tag, WSLCDeleteImageFlagsForce).first);
1827 + }
1828 +
1829 + std::error_code ec;
1830 + std::filesystem::remove_all(contextDir, ec);
1831 + });
1832 +
1833 + {
1834 + std::ofstream dockerfile(contextDir / "Dockerfile");
1835 + dockerfile << "FROM debian:latest\n";
1836 + dockerfile << "CMD [\"echo\", \"multi-tag-ok\"]\n";
1837 + }
1838 + WSLCBuildImageOptions options{.Tags = {tags, 2}};
1839 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options));
1840 + ExpectImagePresent(*m_defaultSession, "wslc-test-multitag:v1");
1841 + ExpectImagePresent(*m_defaultSession, "wslc-test-multitag:v2");
1842 + }
1843 +
1844 + WSLC_TEST_METHOD(BuildImageNullHandle)
1845 + {
1846 + WSLCBuildImageOptions options{.ContextPath = L"C:\\", .DockerfileHandle = {}, .Tags = {nullptr, 0}};
1847 +
1848 + VERIFY_ARE_EQUAL(m_defaultSession->BuildImage(&options, nullptr, nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE));
1849 + }
1850 +
1851 + WSLC_TEST_METHOD(BuildImageCancel)
1852 + {
1853 + class TestProgressCallback
1854 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
1855 + {
1856 + public:
1857 + TestProgressCallback(wil::unique_event& event) : m_event(event)
1858 + {
1859 + }
1860 +
1861 + HRESULT OnProgress(LPCSTR, LPCSTR, ULONGLONG, ULONGLONG) override
1862 + {
1863 + m_event.SetEvent();
1864 + return S_OK;
1865 + }
1866 +
1867 + private:
1868 + wil::unique_event& m_event;
1869 + };
1870 +
1871 + auto contextDir = std::filesystem::current_path() / "build-context-cancel";
1872 + std::filesystem::create_directories(contextDir);
1873 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1874 + std::error_code ec;
1875 + std::filesystem::remove_all(contextDir, ec);
1876 + });
1877 +
1878 + // Use a Dockerfile that takes a long time to build so we can cancel it mid-build.
1879 + {
1880 + std::ofstream dockerfile(contextDir / "Dockerfile");
1881 + dockerfile << "FROM debian:latest\n";
1882 + dockerfile << "RUN sleep 120\n";
1883 + }
1884 +
1885 + wil::unique_event cancelEvent{wil::EventOptions::ManualReset};
1886 + wil::unique_event progressEvent{wil::EventOptions::ManualReset};
1887 +
1888 + // Use a progress callback to detect when the build is actively running
1889 + // before signaling cancellation, avoiding a racy Sleep().
1890 + auto callback = Microsoft::WRL::Make<TestProgressCallback>(progressEvent);
1891 +
1892 + auto contextPathStr = contextDir.wstring();
1893 + auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
1894 +
1895 + LPCSTR tag = "wslc-test-build-cancel:latest";
1896 + WSLCBuildImageOptions options{
1897 + .ContextPath = contextPathStr.c_str(), .DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get()), .Tags = {&tag, 1}};
1898 +
1899 + std::promise<HRESULT> result;
1900 + std::thread buildThread(
1901 + [&]() { result.set_value(m_defaultSession->BuildImage(&options, callback.Get(), cancelEvent.get())); });
1902 +
1903 + auto joinThread = wil::scope_exit([&]() { buildThread.join(); });
1904 +
1905 + VERIFY_IS_TRUE(progressEvent.wait(60 * 1000));
1906 + cancelEvent.SetEvent();
1907 +
1908 + VERIFY_ARE_EQUAL(E_ABORT, result.get_future().get());
1909 + }
1910 +
1911 + WSLC_TEST_METHOD(BuildImageNoCache)
1912 + {
1913 + auto contextDir = std::filesystem::current_path() / "build-context-nocache";
1914 + std::filesystem::create_directories(contextDir);
1915 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1916 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-nocache:latest", WSLCDeleteImageFlagsForce).first);
1917 +
1918 + std::error_code ec;
1919 + std::filesystem::remove_all(contextDir, ec);
1920 + });
1921 +
1922 + {
1923 + std::ofstream dockerfile(contextDir / "Dockerfile");
1924 + dockerfile << "FROM debian:latest\n";
1925 + dockerfile << "RUN echo -n Image && echo -n is && echo -n rebuilt\n";
1926 + }
1927 +
1928 + // First build to populate cache.
1929 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-nocache:latest"));
1930 +
1931 + // Validate that the image isn't rebuilt when NoCache isn't set.
1932 + {
1933 + std::string output;
1934 + auto callback = Microsoft::WRL::Make<CapturingProgressCallback>(output);
1935 + LPCSTR tag = "wslc-test-nocache:latest";
1936 + WSLCBuildImageOptions options{.Tags = {&tag, 1}};
1937 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options, callback.Get()));
1938 + VERIFY_IS_TRUE(output.find("Imageisrebuilt") == std::string::npos);
1939 + }
1940 +
1941 + // Validate that the image is rebuilt when WSLCBuildImageFlagsNoCache is set, and that the output from the RUN step appears in the progress callback.
1942 + {
1943 + std::string output;
1944 + auto callback = Microsoft::WRL::Make<CapturingProgressCallback>(output);
1945 + LPCSTR tag = "wslc-test-nocache:latest";
1946 + WSLCBuildImageOptions options{.Tags = {&tag, 1}, .Flags = WSLCBuildImageFlagsNoCache};
1947 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options, callback.Get()));
1948 + VERIFY_IS_TRUE(output.find("Imageisrebuilt") != std::string::npos);
1949 + }
1950 + }
1951 +
1952 + WSLC_TEST_METHOD(BuildImageInvalidFlags)
1953 + {
1954 + auto dummyDockerfile = wil::create_new_file(
1955 + (std::filesystem::current_path() / "Dockerfile").c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, FILE_FLAG_DELETE_ON_CLOSE);
1956 +
1957 + auto contextDir = std::filesystem::current_path();
1958 +
1959 + WSLCBuildImageOptions options{
1960 + .ContextPath = contextDir.c_str(),
1961 + .DockerfileHandle = ToCOMInputHandle(dummyDockerfile.get()),
1962 + .Flags = static_cast<WSLCBuildImageFlags>(0x8)};
1963 +
1964 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->BuildImage(&options, nullptr, nullptr));
1965 + }
1966 +
1967 + WSLC_TEST_METHOD(AnonymousVolumes)
1968 + {
1969 + auto contextDir = std::filesystem::current_path() / "build-context";
1970 + std::filesystem::create_directories(contextDir);
1971 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1972 + std::error_code ec;
1973 + std::filesystem::remove_all(contextDir, ec);
1974 +
1975 + LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build:latest", WSLCDeleteImageFlagsForce).first);
1976 + });
1977 +
1978 + {
1979 + std::ofstream dockerfile(contextDir / "Dockerfile");
1980 + dockerfile << "FROM debian:latest\n";
1981 + dockerfile << "VOLUME /volume\n"; // Use VOLUME to force the creation of an anonymous volume.
1982 + }
1983 +
1984 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build:latest"));
1985 + ExpectImagePresent(*m_defaultSession, "wslc-test-build:latest");
1986 +
1987 + // Lists anonymous docker volume names via the VM's docker CLI.
1988 + // TODO: Add proper support so we can list via session's API instead.
1989 + auto listAnonymousVolumes = [&]() {
1990 + auto result = ExpectCommandResult(
1991 + m_defaultSession.get(), {"/usr/bin/docker", "volume", "ls", "-q", "-f", "label=com.docker.volume.anonymous"}, 0);
1992 + std::vector<std::string> names;
1993 + std::stringstream ss(result.Output[1]);
1994 + std::string line;
1995 + while (std::getline(ss, line))
1996 + {
1997 + if (!line.empty())
1998 + {
1999 + names.push_back(line);
2000 + }
2001 + }
2002 + return names;
2003 + };
2004 +
2005 + // Session-restart scenario: an anonymous volume-backed container survives a session reset.
2006 + {
2007 + WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-anonymous-volume", {"test", "-d", "/volume"});
2008 + auto container = launcher.Launch(*m_defaultSession);
2009 + auto result = container.GetInitProcess();
2010 +
2011 + auto containerId = container.Id();
2012 +
2013 + ValidateProcessOutput(result, {});
2014 +
2015 + ResetTestSession();
2016 +
2017 + container.SetDeleteOnClose(false);
2018 +
2019 + // Manually cleanup the container and delete anonymous volumes since the session has been reset.
2020 + auto containerCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2021 + wil::com_ptr<IWSLCContainer> container;
2022 + VERIFY_SUCCEEDED(m_defaultSession->OpenContainer(containerId.c_str(), &container));
2023 +
2024 + VERIFY_SUCCEEDED(container->Delete(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes));
2025 + });
2026 +
2027 + // Validate that the session is correctly restarted.
2028 + wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
2029 + wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
2030 +
2031 + VERIFY_SUCCEEDED(
2032 + m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
2033 +
2034 + VERIFY_ARE_EQUAL(containers.size(), 1);
2035 + VERIFY_ARE_EQUAL(containers[0].Id, containerId);
2036 + }
2037 +
2038 + // Delete container without WSLCDeleteFlagsDeleteVolumes -> anonymous volume is leaked.
2039 + {
2040 + WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-delete-vol-leak", {"test", "-d", "/volume"});
2041 + auto container = launcher.Launch(*m_defaultSession);
2042 + container.GetInitProcess().Wait();
2043 + container.SetDeleteOnClose(false);
2044 +
2045 + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2046 +
2047 + VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
2048 +
2049 + // Anonymous volume was NOT deleted by Docker.
2050 + auto leaked = listAnonymousVolumes();
2051 + VERIFY_ARE_EQUAL(leaked.size(), 1u);
2052 +
2053 + RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "volume", "prune", "-f"});
2054 + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u);
2055 + }
2056 +
2057 + // Delete container with WSLCDeleteFlagsDeleteVolumes -> anonymous volume is cleaned up.
2058 + {
2059 + WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-delete-vol-rm", {"sleep", "99999"});
2060 + auto container = launcher.Launch(*m_defaultSession);
2061 + container.SetDeleteOnClose(false);
2062 +
2063 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
2064 +
2065 + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2066 +
2067 + VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsDeleteVolumes));
2068 + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u);
2069 + }
2070 +
2071 + // Container with WSLCContainerFlagsRm -> anonymous volume cleaned up when the container auto-removes on exit.
2072 + {
2073 + WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-delete-vol-rm", {"sleep", "99999"});
2074 + launcher.SetContainerFlags(WSLCContainerFlagsRm);
2075 +
2076 + auto container = launcher.Launch(*m_defaultSession);
2077 + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2078 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
2079 +
2080 + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u);
2081 + }
2082 + }
2083 +
2084 + WSLC_TEST_METHOD(TagImage)
2085 + {
2086 + auto runTagImage = [&](LPCSTR Image, LPCSTR Repo, LPCSTR Tag) {
2087 + WSLCTagImageOptions options{};
2088 + options.Image = Image;
2089 + options.Repo = Repo;
2090 + options.Tag = Tag;
2091 +
2092 + return m_defaultSession->TagImage(&options);
2093 + };
2094 +
2095 + // Positive test: Tag an existing image with a new tag in the same repository.
2096 + {
2097 + ExpectImagePresent(*m_defaultSession, "debian:latest");
2098 +
2099 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2100 + DeleteImage("debian:test-tag", WSLCDeleteImageFlagsNoPrune);
2101 +
2102 + ExpectImagePresent(*m_defaultSession, "debian:test-tag", false);
2103 + ExpectImagePresent(*m_defaultSession, "debian:latest");
2104 + });
2105 +
2106 + VERIFY_SUCCEEDED(runTagImage("debian:latest", "debian", "test-tag"));
2107 +
2108 + // Verify both tags exist and point to the same image.
2109 + ExpectImagePresent(*m_defaultSession, "debian:latest");
2110 + ExpectImagePresent(*m_defaultSession, "debian:test-tag");
2111 +
2112 + // Verify they have the same image hash.
2113 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
2114 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
2115 +
2116 + std::string latestHash;
2117 + std::string testTagHash;
2118 + for (const auto& image : images)
2119 + {
2120 + if (std::strcmp(image.Image, "debian:latest") == 0)
2121 + {
2122 + latestHash = image.Hash;
2123 + }
2124 + else if (std::strcmp(image.Image, "debian:test-tag") == 0)
2125 + {
2126 + testTagHash = image.Hash;
2127 + }
2128 + }
2129 +
2130 + VERIFY_IS_FALSE(latestHash.empty());
2131 + VERIFY_IS_FALSE(testTagHash.empty());
2132 + VERIFY_ARE_EQUAL(latestHash, testTagHash);
2133 + }
2134 +
2135 + // Positive test: Tag with a different repository name.
2136 + {
2137 + ExpectImagePresent(*m_defaultSession, "debian:latest");
2138 +
2139 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2140 + DeleteImage("myrepo/myimage:v1.0.0", WSLCDeleteImageFlagsNoPrune);
2141 +
2142 + ExpectImagePresent(*m_defaultSession, "myrepo/myimage:v1.0.0", false);
2143 + });
2144 +
2145 + VERIFY_SUCCEEDED(runTagImage("debian:latest", "myrepo/myimage", "v1.0.0"));
2146 +
2147 + ExpectImagePresent(*m_defaultSession, "myrepo/myimage:v1.0.0");
2148 + }
2149 +
2150 + // Positive test: Tag using image ID.
2151 + {
2152 + ExpectImagePresent(*m_defaultSession, "debian:latest");
2153 +
2154 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2155 + DeleteImage("debian:test-by-id", WSLCDeleteImageFlagsNoPrune);
2156 +
2157 + ExpectImagePresent(*m_defaultSession, "debian:test-by-id", false);
2158 + });
2159 +
2160 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
2161 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
2162 +
2163 + std::string imageId;
2164 + for (const auto& image : images)
2165 + {
2166 + if (std::strcmp(image.Image, "debian:latest") == 0)
2167 + {
2168 + imageId = image.Hash;
2169 + break;
2170 + }
2171 + }
2172 + VERIFY_IS_FALSE(imageId.empty());
2173 +
2174 + VERIFY_SUCCEEDED(runTagImage(imageId.c_str(), "debian", "test-by-id"));
2175 +
2176 + ExpectImagePresent(*m_defaultSession, "debian:test-by-id");
2177 + }
2178 +
2179 + // Positive test: Overwrite existing tag.
2180 + {
2181 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2182 + DeleteImage("test:duplicate-tag", WSLCDeleteImageFlagsNoPrune);
2183 +
2184 + ExpectImagePresent(*m_defaultSession, "test:duplicate-tag", false);
2185 + });
2186 +
2187 + VERIFY_SUCCEEDED(runTagImage("debian:latest", "test", "duplicate-tag"));
2188 + VERIFY_SUCCEEDED(runTagImage("debian:latest", "test", "duplicate-tag"));
2189 + }
2190 +
2191 + // Negative test: Null options pointer.
2192 + {
2193 + VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER), m_defaultSession->TagImage(nullptr));
2194 + }
2195 +
2196 + // Negative test: Null Image field.
2197 + {
2198 + VERIFY_ARE_EQUAL(E_POINTER, runTagImage(nullptr, "test", "tag"));
2199 + }
2200 +
2201 + // Negative test: Null Repo field.
2202 + {
2203 + VERIFY_ARE_EQUAL(E_POINTER, runTagImage("debian:latest", nullptr, "tag"));
2204 + }
2205 +
2206 + // Negative test: Null Tag field.
2207 + {
2208 + VERIFY_ARE_EQUAL(E_POINTER, runTagImage("debian:latest", "test", nullptr));
2209 + }
2210 +
2211 + // Negative test: Tag a non-existent image.
2212 + {
2213 + VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, runTagImage("nonexistent:notfound", "test", "fail"));
2214 + ValidateCOMErrorMessage(L"No such image: nonexistent:notfound");
2215 + }
2216 +
2217 + // Negative test: Invalid tag format with spaces.
2218 + {
2219 + VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), runTagImage("debian:latest", "test", "invalid tag"));
2220 + ValidateCOMErrorMessage(L"invalid tag format");
2221 + }
2222 + }
2223 +
2224 + WSLC_TEST_METHOD(InspectImage)
2225 + {
2226 + // Test inspect debian:latest
2227 + {
2228 + wil::unique_cotaskmem_ansistring output;
2229 + VERIFY_SUCCEEDED(m_defaultSession->InspectImage("debian:latest", &output));
2230 +
2231 + // Verify output is valid JSON
2232 + VERIFY_IS_NOT_NULL(output.get());
2233 + VERIFY_IS_TRUE(std::strlen(output.get()) > 0);
2234 + LogInfo("Inspect output: %hs", output.get());
2235 +
2236 + // Parse and validate JSON structure
2237 + auto inspectResult = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectImage>(output.get());
2238 +
2239 + // Verify all fields exposed in wslc_schema::InspectImage
2240 + VERIFY_IS_TRUE(inspectResult.Id.find("sha256:") == 0);
2241 +
2242 + VERIFY_IS_TRUE(inspectResult.RepoTags.has_value());
2243 + VERIFY_IS_FALSE(inspectResult.RepoTags->empty());
2244 + bool foundTag = false;
2245 + for (const auto& tag : inspectResult.RepoTags.value())
2246 + {
2247 + if (tag.find("debian:latest") != std::string::npos)
2248 + {
2249 + foundTag = true;
2250 + break;
2251 + }
2252 + }
2253 + VERIFY_IS_TRUE(foundTag);
2254 +
2255 + // skip testing RepoDigests for loaded test image.
2256 + VERIFY_IS_FALSE(inspectResult.Created.empty());
2257 + VERIFY_IS_TRUE(inspectResult.Architecture == "amd64" || inspectResult.Architecture == "arm64");
2258 + VERIFY_ARE_EQUAL("linux", inspectResult.Os);
2259 + VERIFY_IS_TRUE(inspectResult.Size > 0);
2260 + VERIFY_IS_TRUE(inspectResult.Metadata.has_value());
2261 + VERIFY_IS_TRUE(inspectResult.Metadata->size() > 0);
2262 +
2263 + VERIFY_IS_TRUE(inspectResult.Config.has_value());
2264 + const auto& config = inspectResult.Config.value();
2265 + VERIFY_IS_TRUE(config.Cmd.has_value());
2266 + VERIFY_IS_TRUE(config.Cmd->size() > 0);
2267 + VERIFY_IS_TRUE(config.Entrypoint.has_value());
2268 + VERIFY_ARE_EQUAL(0, config.Entrypoint->size());
2269 + VERIFY_IS_TRUE(config.Env.has_value());
2270 + VERIFY_IS_TRUE(config.Env->size() > 0);
2271 + VERIFY_IS_FALSE(config.Labels.has_value());
2272 + }
2273 +
2274 + // Negative test: Image not found
2275 + {
2276 + wil::unique_cotaskmem_ansistring output;
2277 + VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, m_defaultSession->InspectImage("nonexistent:image", &output));
2278 + ValidateCOMErrorMessage(L"No such image: nonexistent:image");
2279 + }
2280 +
2281 + // Negative test: Bad image name input
2282 + {
2283 + wil::unique_cotaskmem_ansistring output;
2284 +
2285 + std::string longImageName(WSLC_MAX_IMAGE_NAME_LENGTH + 1, 'a');
2286 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->InspectImage(longImageName.c_str(), &output));
2287 +
2288 + // Invalid name.
2289 + VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), m_defaultSession->InspectImage("debian latest", &output));
2290 + ValidateCOMErrorMessage(L"invalid reference format");
2291 +
2292 + // Attempt to fake to call search endpoint. Our implementation escaped the image name correctly.
2293 + VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, m_defaultSession->InspectImage("search/debian:latest", &output));
2294 + ValidateCOMErrorMessage(L"No such image: search/debian:latest");
2295 + }
2296 + }
2297 +
2298 + struct BlockingOperation
2299 + {
2300 + NON_COPYABLE(BlockingOperation);
2301 + NON_MOVABLE(BlockingOperation);
2302 +
2303 + BlockingOperation(std::function<HRESULT(HANDLE)>&& Operation, HRESULT ExpectedResult = S_OK, bool AllowEarlyCompletion = false, bool UseOverlappedWritePipe = false) :
2304 + m_operation(std::move(Operation)), m_expectedResult(ExpectedResult), m_allowEarlyCompletion(AllowEarlyCompletion)
2305 + {
2306 + auto [pipeRead, pipeWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(100000, false, UseOverlappedWritePipe);
2307 +
2308 + m_operationThread = std::thread(&BlockingOperation::RunOperation, this, std::move(pipeWrite));
2309 + m_ioThread = std::thread(&BlockingOperation::RunIO, this, std::move(pipeRead));
2310 +
2311 + // Wait for the operation to be running before continuing.
2312 + VERIFY_IS_TRUE(m_startedEvent.wait(60 * 1000));
2313 + }
2314 +
2315 + ~BlockingOperation()
2316 + {
2317 + if (m_operationThread.joinable())
2318 + {
2319 + m_operationThread.join();
2320 + }
2321 +
2322 + if (m_ioThread.joinable())
2323 + {
2324 + m_ioThread.join();
2325 + }
2326 + }
2327 +
2328 + void RunOperation(wil::unique_hfile Handle)
2329 + {
2330 + m_result.set_value(m_operation(Handle.get()));
2331 +
2332 + // Fail if the operation completed before the test signaled completion
2333 + // (unless early completion is expected, e.g. session termination).
2334 + // Don't use VERIFY macros since this is running in a separate thread.
2335 + WI_ASSERT(m_allowEarlyCompletion || m_testCompleteEvent.is_signaled());
2336 + }
2337 +
2338 + void RunIO(wil::unique_hfile Handle)
2339 + {
2340 + std::vector<char> buffer(1024 * 1024);
2341 + while (true)
2342 + {
2343 + DWORD bytesRead{};
2344 + if (!ReadFile(Handle.get(), buffer.data(), static_cast<DWORD>(buffer.size()), &bytesRead, nullptr))
2345 + {
2346 + if (GetLastError() != ERROR_BROKEN_PIPE)
2347 + {
2348 + LogError("Unexpected ReadFile() error: %u", GetLastError());
2349 + }
2350 +
2351 + break;
2352 + }
2353 +
2354 + if (bytesRead == 0)
2355 + {
2356 + break;
2357 + }
2358 +
2359 + if (!m_startedEvent.is_signaled())
2360 + {
2361 + m_startedEvent.SetEvent();
2362 + }
2363 +
2364 + // Block until the test completes.
2365 + if (!m_testCompleteEvent.wait(60 * 1000))
2366 + {
2367 + LogError("Timed out waiting for test completion");
2368 + break;
2369 + }
2370 + }
2371 + }
2372 +
2373 + void Complete()
2374 + {
2375 + m_testCompleteEvent.SetEvent();
2376 +
2377 + VERIFY_ARE_EQUAL(m_expectedResult, m_result.get_future().get());
2378 + }
2379 +
2380 + std::function<HRESULT(HANDLE)> m_operation;
2381 + wil::unique_event m_startedEvent{wil::EventOptions::ManualReset};
2382 + wil::unique_event m_testCompleteEvent{wil::EventOptions::ManualReset};
2383 + std::thread m_operationThread;
2384 + std::thread m_ioThread;
2385 + std::promise<HRESULT> m_result;
2386 + HRESULT m_expectedResult{};
2387 + bool m_allowEarlyCompletion{};
2388 + };
2389 +
2390 + WSLC_TEST_METHOD(SaveImage)
2391 + {
2392 + {
2393 + std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
2394 + wil::unique_handle imageTarFileHandle{
2395 + CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2396 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2397 + LARGE_INTEGER fileSize{};
2398 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2399 + // Load the image from a saved tar
2400 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
2401 + // Verify that the image is in the list of images.
2402 + ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2403 + WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
2404 + auto container = launcher.Launch(*m_defaultSession);
2405 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
2406 + VERIFY_ARE_EQUAL(0, result.Code);
2407 + VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
2408 + }
2409 +
2410 + {
2411 + std::filesystem::path imageTar = L"HelloWorldExported.tar";
2412 + auto cleanup =
2413 + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
2414 + // Save the image to a tar file.
2415 + {
2416 + wil::unique_handle imageTarFileHandle{CreateFileW(
2417 + imageTar.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2418 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2419 + LARGE_INTEGER fileSize{};
2420 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2421 + VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, false);
2422 + VERIFY_SUCCEEDED(m_defaultSession->SaveImage(ToCOMInputHandle(imageTarFileHandle.get()), "hello-world:latest", nullptr, nullptr));
2423 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2424 + VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, true);
2425 + }
2426 +
2427 + // Load the saved image to verify it's valid.
2428 + {
2429 + wil::unique_handle imageTarFileHandle{CreateFileW(
2430 + imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2431 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2432 + LARGE_INTEGER fileSize{};
2433 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2434 + // Load the image from a saved tar
2435 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
2436 + // Verify that the image is in the list of images.
2437 + ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2438 + WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
2439 + auto container = launcher.Launch(*m_defaultSession);
2440 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
2441 + VERIFY_ARE_EQUAL(0, result.Code);
2442 + VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
2443 + }
2444 + }
2445 +
2446 + // Try to save an invalid image.
2447 + {
2448 + std::filesystem::path imageTar = L"HelloWorldError.tar";
2449 + auto cleanfile =
2450 + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
2451 + wil::unique_handle imageTarFileHandle{CreateFileW(
2452 + imageTar.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2453 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2454 + LARGE_INTEGER fileSize{};
2455 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2456 + VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, false);
2457 + VERIFY_FAILED(m_defaultSession->SaveImage(ToCOMInputHandle(imageTarFileHandle.get()), "hello-wld:latest", nullptr, nullptr));
2458 + ValidateCOMErrorMessage(L"reference does not exist");
2459 +
2460 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2461 + VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, false);
2462 + }
2463 +
2464 + // Validate that cancellation works.
2465 + {
2466 + wil::unique_event cancelEvent{wil::EventOptions::ManualReset};
2467 +
2468 + BlockingOperation operation(
2469 + [&](HANDLE handle) {
2470 + return m_defaultSession->SaveImage(ToCOMInputHandle(handle), "debian:latest", nullptr, cancelEvent.get());
2471 + },
2472 + E_ABORT);
2473 +
2474 + cancelEvent.SetEvent();
2475 + operation.Complete();
2476 + }
2477 + }
2478 +
2479 + WSLC_TEST_METHOD(SynchronousIoCancellation)
2480 + {
2481 + // Create a blocked operation that will cause the service to get stuck on a ReadFile() call.
2482 + // Because the pipe handle that we're passing in doesn't support overlapped IO, the service will get stuck in a
2483 + // synchronous ReadFile() call. Validate that terminating the session correctly cancels the IO.
2484 +
2485 + wil::unique_handle pipeRead;
2486 + wil::unique_handle pipeWrite;
2487 + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
2488 +
2489 + std::promise<HRESULT> result;
2490 +
2491 + wil::unique_event testCompleted{wil::EventOptions::ManualReset};
2492 + std::thread operationThread([&]() {
2493 + result.set_value(m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "dummy:latest", nullptr, 1024 * 1024));
2494 +
2495 + WI_ASSERT(testCompleted.is_signaled()); // Sanity check.
2496 + });
2497 +
2498 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
2499 +
2500 + // Write 4 bytes to validate that the service has started reading from the pipe (since the pipe buffer is 2).
2501 + DWORD bytesWritten{};
2502 + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
2503 +
2504 + testCompleted.SetEvent();
2505 +
2506 + // N.B. It's not possible to deterministically wait for the service to be stuck in the ReadFile() call.
2507 + // It's possible that the service will check the session termination event before calling ReadFile() on the pipe
2508 + // but that's OK since we can also accept that error code here (E_ABORT).
2509 + VERIFY_SUCCEEDED(m_defaultSession->Terminate());
2510 +
2511 + auto reset = ResetTestSession();
2512 +
2513 + auto hr = result.get_future().get();
2514 + if (hr != E_ABORT && hr != HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED))
2515 + {
2516 + LogError("Unexpected result: 0x%08X", hr);
2517 + VERIFY_FAIL();
2518 + }
2519 + }
2520 +
2521 + WSLC_TEST_METHOD(ExportContainer)
2522 + {
2523 + // Load an image and launch a container to verify image is valid.
2524 + // Then export the container to a tar file.
2525 + // Load the exported tar file to verify it's a valid image and can be launched.
2526 + // Finally, stop and delete the container, then try to export again to verify it fails as expected.
2527 + {
2528 + std::filesystem::path containerTar = L"HelloWorldExported.tar";
2529 + auto cleanup =
2530 + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(containerTar.c_str())); });
2531 +
2532 + // Load the image from a saved tar and launch a container
2533 + {
2534 + std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
2535 + wil::unique_handle imageTarFileHandle{CreateFileW(
2536 + imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2537 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2538 + LARGE_INTEGER fileSize{};
2539 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2540 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
2541 + // Verify that the image is in the list of images.
2542 + ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2543 + WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
2544 + auto container = launcher.Launch(*m_defaultSession);
2545 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
2546 + VERIFY_ARE_EQUAL(0, result.Code);
2547 + VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
2548 +
2549 + // Export the container to a tar file.
2550 + wil::unique_handle containerTarFileHandle{CreateFileW(
2551 + containerTar.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2552 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == containerTarFileHandle.get());
2553 + VERIFY_IS_TRUE(GetFileSizeEx(containerTarFileHandle.get(), &fileSize));
2554 + VERIFY_ARE_EQUAL(fileSize.QuadPart, 0);
2555 + VERIFY_SUCCEEDED(container.Get().Export(ToCOMInputHandle(containerTarFileHandle.get())));
2556 + VERIFY_IS_TRUE(GetFileSizeEx(containerTarFileHandle.get(), &fileSize));
2557 + VERIFY_ARE_NOT_EQUAL(fileSize.QuadPart, 0);
2558 + }
2559 +
2560 + // Load the exported container to verify it's valid.
2561 + {
2562 + wil::unique_handle containerTarFileHandle{CreateFileW(
2563 + containerTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2564 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == containerTarFileHandle.get());
2565 + LARGE_INTEGER fileSize{};
2566 + VERIFY_IS_TRUE(GetFileSizeEx(containerTarFileHandle.get(), &fileSize));
2567 +
2568 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2569 + LOG_IF_FAILED(DeleteImageNoThrow("test-imported-container:latest", WSLCDeleteImageFlagsNone).first);
2570 + });
2571 +
2572 + VERIFY_SUCCEEDED(m_defaultSession->ImportImage(
2573 + ToCOMInputHandle(containerTarFileHandle.get()), "test-imported-container:latest", nullptr, fileSize.QuadPart));
2574 +
2575 + // Verify that the image is in the list of images.
2576 + ExpectImagePresent(*m_defaultSession, "test-imported-container:latest");
2577 + WSLCContainerLauncher launcher("test-imported-container:latest", "wslc-hello-world-container", {"/hello"});
2578 + auto container = launcher.Launch(*m_defaultSession);
2579 + auto result = container.GetInitProcess().WaitAndCaptureOutput();
2580 + VERIFY_ARE_EQUAL(0, result.Code);
2581 + VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
2582 +
2583 + // Stop and delete the above container and try to export.
2584 +
2585 + std::filesystem::path imageTarFile = L"HelloWorldExportError.tar";
2586 + auto cleanfile =
2587 + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTarFile.c_str())); });
2588 + wil::unique_handle contTarFileHandle{CreateFileW(
2589 + imageTarFile.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2590 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == contTarFileHandle.get());
2591 + VERIFY_IS_TRUE(GetFileSizeEx(contTarFileHandle.get(), &fileSize));
2592 + VERIFY_ARE_EQUAL(fileSize.QuadPart, 0);
2593 +
2594 + auto outFile = ToCOMInputHandle(contTarFileHandle.get());
2595 +
2596 + container.Get().Stop(WSLCSignalSIGILL, 10);
2597 + container.Get().Delete(WSLCDeleteFlagsNone);
2598 + VERIFY_ARE_EQUAL(container.Get().Export(outFile), RPC_E_DISCONNECTED);
2599 +
2600 + VERIFY_IS_TRUE(GetFileSizeEx(contTarFileHandle.get(), &fileSize));
2601 + VERIFY_ARE_EQUAL(fileSize.QuadPart, 0);
2602 + }
2603 + }
2604 + }
2605 +
2606 + WSLC_TEST_METHOD(CustomDmesgOutput)
2607 + {
2608 + SKIP_TEST_ARM64();
2609 +
2610 + auto createVmWithDmesg = [this](bool earlyBootLogging) {
2611 + auto [read, write] = CreateSubprocessPipe(false, false);
2612 +
2613 + auto settings = GetDefaultSessionSettings(L"dmesg-output-test");
2614 + settings.DmesgOutput = ToCOMInputHandle(write.get());
2615 + WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsEarlyBootDmesg, earlyBootLogging);
2616 +
2617 + std::vector<char> dmesgContent;
2618 + auto readDmesg = [read = read.get(), &dmesgContent]() mutable {
2619 + DWORD Offset = 0;
2620 +
2621 + constexpr auto bufferSize = 1024;
2622 + while (true)
2623 + {
2624 + dmesgContent.resize(Offset + bufferSize);
2625 +
2626 + DWORD Read{};
2627 + if (!ReadFile(read, &dmesgContent[Offset], bufferSize, &Read, nullptr))
2628 + {
2629 + LogInfo("ReadFile() failed: %lu", GetLastError());
2630 + }
2631 +
2632 + if (Read == 0)
2633 + {
2634 + break;
2635 + }
2636 +
2637 + Offset += Read;
2638 + }
2639 + };
2640 +
2641 + std::thread thread(readDmesg); // Needs to be created before the VM starts, to avoid a pipe deadlock.
2642 +
2643 + // Ensure the thread is joined even if CreateSession throws, to avoid std::terminate.
2644 + auto threadGuard = wil::scope_exit([&]() {
2645 + write.reset();
2646 + if (thread.joinable())
2647 + {
2648 + thread.join();
2649 + }
2650 + });
2651 +
2652 + auto session = CreateSession(settings);
2653 + threadGuard.release(); // CreateSession succeeded, detach scope_exit below takes over.
2654 +
2655 + auto detach = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2656 + session.reset();
2657 + if (thread.joinable())
2658 + {
2659 + thread.join();
2660 + }
2661 + });
2662 +
2663 + write.reset();
2664 +
2665 + ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo DmesgTest > /dev/kmsg"}, 0);
2666 +
2667 + session.reset();
2668 + detach.reset();
2669 +
2670 + auto contentString = std::string(dmesgContent.begin(), dmesgContent.end());
2671 +
2672 + VERIFY_ARE_NOT_EQUAL(contentString.find("Run /init as init process"), std::string::npos);
2673 + VERIFY_ARE_NOT_EQUAL(contentString.find("DmesgTest"), std::string::npos);
2674 +
2675 + return contentString;
2676 + };
2677 +
2678 + auto validateFirstDmesgLine = [](const std::string& dmesg, const char* expected) {
2679 + auto firstLf = dmesg.find("\n");
2680 + VERIFY_ARE_NOT_EQUAL(firstLf, std::string::npos);
2681 + VERIFY_IS_TRUE(dmesg.find(expected) < firstLf);
2682 + };
2683 +
2684 + // Dmesg without early boot logging
2685 + {
2686 + auto dmesg = createVmWithDmesg(false);
2687 +
2688 + // Verify that the first line is "brd: module loaded";
2689 + validateFirstDmesgLine(dmesg, "brd: module loaded");
2690 + }
2691 +
2692 + // Dmesg with early boot logging
2693 + {
2694 + auto dmesg = createVmWithDmesg(true);
2695 + validateFirstDmesgLine(dmesg, "Linux version");
2696 + }
2697 + }
2698 +
2699 + WSLC_TEST_METHOD(TerminationCallback)
2700 + {
2701 + class DECLSPEC_UUID("7BC4E198-6531-4FA6-ADE2-5EF3D2A04DFF") CallbackInstance
2702 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, ITerminationCallback, IFastRundown>
2703 + {
2704 +
2705 + public:
2706 + CallbackInstance(std::function<void(WSLCVirtualMachineTerminationReason, LPCWSTR)>&& callback) :
2707 + m_callback(std::move(callback))
2708 + {
2709 + }
2710 +
2711 + HRESULT OnTermination(WSLCVirtualMachineTerminationReason Reason, LPCWSTR Details) override
2712 + {
2713 + m_callback(Reason, Details);
2714 + return S_OK;
2715 + }
2716 +
2717 + private:
2718 + std::function<void(WSLCVirtualMachineTerminationReason, LPCWSTR)> m_callback;
2719 + };
2720 +
2721 + std::promise<std::pair<WSLCVirtualMachineTerminationReason, std::wstring>> promise;
2722 +
2723 + CallbackInstance callback{[&](WSLCVirtualMachineTerminationReason reason, LPCWSTR details) {
2724 + promise.set_value(std::make_pair(reason, details));
2725 + }};
2726 +
2727 + WSLCSessionSettings sessionSettings = GetDefaultSessionSettings(L"termination-callback-test");
2728 + sessionSettings.TerminationCallback = &callback;
2729 +
2730 + auto session = CreateSession(sessionSettings);
2731 +
2732 + session.reset();
2733 + auto future = promise.get_future();
2734 + auto result = future.wait_for(std::chrono::seconds(30));
2735 + VERIFY_ARE_EQUAL(result, std::future_status::ready);
2736 + auto [reason, details] = future.get();
2737 + VERIFY_ARE_EQUAL(reason, WSLCVirtualMachineTerminationReasonShutdown);
2738 + VERIFY_ARE_NOT_EQUAL(details, L"");
2739 + }
2740 +
2741 + WSLC_TEST_METHOD(BuildImageStuckCallbackCancellation)
2742 + {
2743 + class StuckBuildProgressCallback
2744 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
2745 + {
2746 + public:
2747 + StuckBuildProgressCallback(std::promise<void>& reachedPromise, wil::unique_event& exitEvent) :
2748 + m_reachedPromise(reachedPromise), m_exitEvent(exitEvent)
2749 + {
2750 + }
2751 +
2752 + HRESULT OnProgress(LPCSTR, LPCSTR, ULONGLONG, ULONGLONG) override
2753 + {
2754 + if (!m_signaled)
2755 + {
2756 + m_signaled = true;
2757 + m_reachedPromise.set_value();
2758 + m_exitEvent.wait(); // Block until this test case is complete.
2759 + }
2760 +
2761 + return S_OK;
2762 + }
2763 +
2764 + private:
2765 + std::promise<void>& m_reachedPromise;
2766 + wil::unique_event& m_exitEvent;
2767 + bool m_signaled{};
2768 + };
2769 +
2770 + auto contextDir = std::filesystem::current_path() / "build-context-stuck-callback";
2771 + std::filesystem::create_directories(contextDir);
2772 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2773 + std::error_code ec;
2774 + std::filesystem::remove_all(contextDir, ec);
2775 + });
2776 +
2777 + {
2778 + std::ofstream dockerfile(contextDir / "Dockerfile");
2779 + dockerfile << "FROM debian:latest\n";
2780 + dockerfile << "RUN echo hello\n";
2781 + }
2782 +
2783 + auto contextPathStr = contextDir.wstring();
2784 + auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
2785 +
2786 + WSLCBuildImageOptions options{
2787 + .ContextPath = contextPathStr.c_str(),
2788 + .DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get()),
2789 + .Flags = WSLCBuildImageFlagsVerbose,
2790 + };
2791 +
2792 + std::promise<void> callbackReached;
2793 + wil::unique_event exitEvent{wil::EventOptions::ManualReset};
2794 + auto callback = Microsoft::WRL::Make<StuckBuildProgressCallback>(callbackReached, exitEvent);
2795 +
2796 + std::promise<HRESULT> buildResult;
2797 + std::thread buildThread(
2798 + [&]() { buildResult.set_value(m_defaultSession->BuildImage(&options, callback.Get(), exitEvent.get())); });
2799 +
2800 + auto joinThread = wil::scope_exit([&]() {
2801 + exitEvent.SetEvent();
2802 + buildThread.join();
2803 + });
2804 +
2805 + // Wait for the progress callback to be called, proving the COM call is in flight.
2806 + auto reachedFuture = callbackReached.get_future();
2807 + auto reachedStatus = reachedFuture.wait_for(std::chrono::seconds(60));
2808 + VERIFY_ARE_EQUAL(reachedStatus, std::future_status::ready);
2809 +
2810 + // Terminate the session while the callback is stuck.
2811 + // This should cancel the pending COM call and unblock BuildImage.
2812 + VERIFY_SUCCEEDED(m_defaultSession->Terminate());
2813 + ResetTestSession();
2814 +
2815 + auto buildFuture = buildResult.get_future();
2816 + auto buildStatus = buildFuture.wait_for(std::chrono::seconds(60));
2817 + VERIFY_ARE_EQUAL(buildStatus, std::future_status::ready);
2818 +
2819 + // BuildImage should have failed due to COM call cancellation.
2820 + VERIFY_FAILED(buildFuture.get());
2821 + }
2822 +
2823 + WSLC_TEST_METHOD(InteractiveShell)
2824 + {
2825 + WSLCProcessLauncher launcher("/bin/sh", {"/bin/sh"}, {"TERM=xterm-256color"}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin);
2826 + auto process = launcher.Launch(*m_defaultSession);
2827 +
2828 + wil::unique_handle tty = process.GetStdHandle(WSLCFDTty);
2829 +
2830 + auto validateTtyOutput = [&](const std::string& expected) {
2831 + std::string buffer(expected.size(), '\0');
2832 +
2833 + DWORD offset = 0;
2834 +
2835 + while (offset < buffer.size())
2836 + {
2837 + DWORD bytesRead{};
2838 + VERIFY_IS_TRUE(ReadFile(tty.get(), buffer.data() + offset, static_cast<DWORD>(buffer.size() - offset), &bytesRead, nullptr));
2839 +
2840 + offset += bytesRead;
2841 + }
2842 +
2843 + buffer.resize(offset);
2844 + VERIFY_ARE_EQUAL(buffer, expected);
2845 + };
2846 +
2847 + auto writeTty = [&](const std::string& content) {
2848 + VERIFY_IS_TRUE(WriteFile(tty.get(), content.data(), static_cast<DWORD>(content.size()), nullptr, nullptr));
2849 + };
2850 +
2851 + // Expect the shell prompt to be displayed
2852 + validateTtyOutput("\033[?2004hsh-5.2# ");
2853 + writeTty("echo OK\n");
2854 + validateTtyOutput("echo OK\r\n\033[?2004l\rOK");
2855 +
2856 + // Exit the shell
2857 + writeTty("exit\n");
2858 +
2859 + VERIFY_IS_TRUE(process.GetExitEvent().wait(30 * 1000));
2860 + }
2861 +
2862 + void ValidateNetworking(WSLCNetworkingMode mode, bool enableDnsTunneling = false)
2863 + {
2864 + // Reuse the default session if settings match (same networking mode and DNS tunneling setting).
2865 + auto createNewSession = mode != m_defaultSessionSettings.NetworkingMode ||
2866 + enableDnsTunneling != WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsDnsTunneling);
2867 +
2868 + auto settings = GetDefaultSessionSettings(L"networking-test", false, mode);
2869 + WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsDnsTunneling, enableDnsTunneling);
2870 + auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
2871 +
2872 + // Validate that eth0 has an ip address
2873 + ExpectCommandResult(
2874 + session.get(),
2875 + {"/bin/sh",
2876 + "-c",
2877 + "ip a show dev eth0 | grep -iF 'inet ' | grep -E '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}'"},
2878 + 0);
2879 +
2880 + ExpectCommandResult(session.get(), {"/bin/grep", "-iF", "nameserver", "/etc/resolv.conf"}, 0);
2881 +
2882 + // Verify that /etc/resolv.conf is correctly configured.
2883 + if (enableDnsTunneling)
2884 + {
2885 + auto result = ExpectCommandResult(session.get(), {"/bin/grep", "-iF", "nameserver ", "/etc/resolv.conf"}, 0);
2886 +
2887 + VERIFY_ARE_EQUAL(result.Output[1], std::format("nameserver {}\n", LX_INIT_DNS_TUNNELING_IP_ADDRESS));
2888 + }
2889 +
2890 + // Verify DNS resolution.
2891 + // Note: without DNS tunneling, NAT mode uses the ICS SharedAccess DNS proxy which only supports UDP.
2892 + // TCP DNS queries (dig +tcp) will time out without tunneling.
2893 + VerifyDigDnsResolution(session.get(), "getent ahosts bing.com");
2894 + VerifyDnsQueries(session.get(), mode, enableDnsTunneling);
2895 + }
2896 +
2897 + TEST_METHOD(NATNetworking)
2898 + {
2899 + ValidateNetworking(WSLCNetworkingModeNAT);
2900 + }
2901 +
2902 + TEST_METHOD(NATNetworkingWithDnsTunneling)
2903 + {
2904 + WINDOWS_11_TEST_ONLY();
2905 + ValidateNetworking(WSLCNetworkingModeNAT, true);
2906 + }
2907 +
2908 + TEST_METHOD(VirtioProxyNetworking)
2909 + {
2910 + ValidateNetworking(WSLCNetworkingModeVirtioProxy);
2911 + }
2912 +
2913 + TEST_METHOD(VirtioProxyNetworkingWithDnsTunneling)
2914 + {
2915 + WINDOWS_11_TEST_ONLY();
2916 + ValidateNetworking(WSLCNetworkingModeVirtioProxy, true);
2917 + }
2918 +
2919 + // DNS test helpers
2920 +
2921 + void VerifyDigDnsResolution(IWSLCSession* session, const std::string& digCommandLine)
2922 + {
2923 + auto result = ExpectCommandResult(session, {"/bin/sh", "-c", digCommandLine}, 0);
2924 + VERIFY_IS_FALSE(result.Output[1].empty());
2925 + }
2926 +
2927 + void VerifyDnsQueries(IWSLCSession* session, WSLCNetworkingMode mode, bool enableDnsTunneling)
2928 + {
2929 + // TCP DNS works except for NAT without tunneling (ICS SharedAccess DNS proxy is UDP-only).
2930 + const bool includeTcp = (mode != WSLCNetworkingModeNAT) || enableDnsTunneling;
2931 +
2932 + // UDP queries for all record types
2933 + VerifyDigDnsResolution(session, "dig +short +time=5 A bing.com");
2934 + VerifyDigDnsResolution(session, "dig +short +time=5 AAAA bing.com");
2935 + VerifyDigDnsResolution(session, "dig +short +time=5 MX bing.com");
2936 + VerifyDigDnsResolution(session, "dig +short +time=5 NS bing.com");
2937 + VerifyDigDnsResolution(session, "dig +short +time=5 -x 8.8.8.8");
2938 + VerifyDigDnsResolution(session, "dig +short +time=5 SOA bing.com");
2939 + VerifyDigDnsResolution(session, "dig +short +time=5 TXT bing.com");
2940 + VerifyDigDnsResolution(session, "dig +time=5 CNAME bing.com");
2941 + VerifyDigDnsResolution(session, "dig +time=5 SRV bing.com");
2942 +
2943 + if (includeTcp)
2944 + {
2945 + // ANY - dig expects a large response so it queries directly over TCP
2946 + VerifyDigDnsResolution(session, "dig +short +time=5 ANY bing.com");
2947 +
2948 + VerifyDigDnsResolution(session, "dig +tcp +short +time=5 A bing.com");
2949 + VerifyDigDnsResolution(session, "dig +tcp +short +time=5 AAAA bing.com");
2950 + VerifyDigDnsResolution(session, "dig +tcp +short +time=5 MX bing.com");
2951 + VerifyDigDnsResolution(session, "dig +tcp +short +time=5 NS bing.com");
2952 + VerifyDigDnsResolution(session, "dig +tcp +short +time=5 -x 8.8.8.8");
2953 + VerifyDigDnsResolution(session, "dig +tcp +short +time=5 SOA bing.com");
2954 + VerifyDigDnsResolution(session, "dig +tcp +short +time=5 TXT bing.com");
2955 + VerifyDigDnsResolution(session, "dig +tcp +time=5 CNAME bing.com");
2956 + VerifyDigDnsResolution(session, "dig +tcp +time=5 SRV bing.com");
2957 + }
2958 + }
2959 +
2960 + void ValidatePortMapping(WSLCNetworkingMode networkingMode)
2961 + {
2962 + auto settings = GetDefaultSessionSettings(L"port-mapping-test");
2963 + settings.NetworkingMode = networkingMode;
2964 +
2965 + // Reuse the default session if the networking mode matches.
2966 + auto createNewSession = networkingMode != m_defaultSessionSettings.NetworkingMode;
2967 + auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
2968 +
2969 + // Install socat in the container.
2970 + //
2971 + // TODO: revisit this in the future to avoid pulling packages from the network.
2972 + auto installSocat = WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "tdnf install socat -y"}).Launch(*session);
2973 + ValidateProcessOutput(installSocat, {}, 0, 300 * 1000);
2974 +
2975 + auto listen = [&](short port, const char* content, bool ipv6) {
2976 + auto cmd = std::format("echo -n '{}' | /usr/bin/socat -dd TCP{}-LISTEN:{},reuseaddr -", content, ipv6 ? "6" : "", port);
2977 + auto process = WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", cmd}).Launch(*session);
2978 + WaitForOutput(process.GetStdHandle(2), "listening on");
2979 +
2980 + return process;
2981 + };
2982 +
2983 + auto connectAndRead = [&](short port, int family) -> std::string {
2984 + SOCKADDR_INET addr{};
2985 + addr.si_family = family;
2986 + INETADDR_SETLOOPBACK((PSOCKADDR)&addr);
2987 + SS_PORT(&addr) = htons(port);
2988 +
2989 + wil::unique_socket hostSocket{socket(family, SOCK_STREAM, IPPROTO_TCP)};
2990 + THROW_LAST_ERROR_IF(!hostSocket);
2991 + THROW_LAST_ERROR_IF(connect(hostSocket.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
2992 +
2993 + return ReadToString(hostSocket.get());
2994 + };
2995 +
2996 + auto expectContent = [&](short port, int family, const char* expected) {
2997 + auto content = connectAndRead(port, family);
2998 + VERIFY_ARE_EQUAL(content, expected);
2999 + };
3000 +
3001 + auto expectNotBound = [&](short port, int family) {
3002 + auto result = wil::ResultFromException([&]() { connectAndRead(port, family); });
3003 +
3004 + VERIFY_ARE_EQUAL(result, HRESULT_FROM_WIN32(WSAECONNREFUSED));
3005 + };
3006 +
3007 + // Map port
3008 + VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, 1234, 80));
3009 +
3010 + // Validate that the same port can't be bound twice
3011 + VERIFY_ARE_EQUAL(session->MapVmPort(AF_INET, 1234, 80), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3012 +
3013 + // Check simple case
3014 + listen(80, "port80", false);
3015 + expectContent(1234, AF_INET, "port80");
3016 +
3017 + // Validate that same port mapping can be reused
3018 + listen(80, "port80", false);
3019 + expectContent(1234, AF_INET, "port80");
3020 +
3021 + // Validate that the connection is immediately reset if the port is not bound on the linux side
3022 + expectContent(1234, AF_INET, "");
3023 +
3024 + // Add a ipv6 binding
3025 + VERIFY_SUCCEEDED(session->MapVmPort(AF_INET6, 1234, 80));
3026 +
3027 + // Validate that ipv6 bindings work as well.
3028 + listen(80, "port80ipv6", true);
3029 + expectContent(1234, AF_INET6, "port80ipv6");
3030 +
3031 + // Unmap the ipv4 port
3032 + VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, 1234, 80));
3033 +
3034 + // Verify that a proper error is returned if the mapping doesn't exist
3035 + VERIFY_ARE_EQUAL(session->UnmapVmPort(AF_INET, 1234, 80), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3036 +
3037 + // Unmap the v6 port
3038 + VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET6, 1234, 80));
3039 +
3040 + // Map another port as v6 only
3041 + VERIFY_SUCCEEDED(session->MapVmPort(AF_INET6, 1235, 81));
3042 +
3043 + listen(81, "port81ipv6", true);
3044 + expectContent(1235, AF_INET6, "port81ipv6");
3045 + expectNotBound(1235, AF_INET);
3046 +
3047 + VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET6, 1235, 81));
3048 + VERIFY_ARE_EQUAL(session->UnmapVmPort(AF_INET6, 1235, 81), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3049 + expectNotBound(1235, AF_INET6);
3050 +
3051 + // Create a forking relay and stress test
3052 + VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, 1234, 80));
3053 +
3054 + auto process =
3055 + WSLCProcessLauncher{"/usr/bin/socat", {"/usr/bin/socat", "-dd", "TCP-LISTEN:80,fork,reuseaddr", "system:'echo -n OK'"}}
3056 + .Launch(*session);
3057 +
3058 + WaitForOutput(process.GetStdHandle(2), "listening on");
3059 +
3060 + for (auto i = 0; i < 100; i++)
3061 + {
3062 + expectContent(1234, AF_INET, "OK");
3063 + }
3064 +
3065 + VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, 1234, 80));
3066 +
3067 + // Validate the 63-port limit.
3068 + // TODO: Remove the 63-port limit by switching the relay's AcceptThread from
3069 + // WaitForMultipleObjects to IO completion ports or similar.
3070 + constexpr int c_maxPorts = 63;
3071 + for (int i = 0; i < c_maxPorts; i++)
3072 + {
3073 + VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + i), static_cast<uint16_t>(80 + i)));
3074 + }
3075 +
3076 + VERIFY_ARE_EQUAL(
3077 + session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + c_maxPorts), static_cast<uint16_t>(80 + c_maxPorts)),
3078 + HRESULT_FROM_WIN32(ERROR_TOO_MANY_OPEN_FILES));
3079 +
3080 + for (int i = 0; i < c_maxPorts; i++)
3081 + {
3082 + VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, static_cast<uint16_t>(20000 + i), static_cast<uint16_t>(80 + i)));
3083 + }
3084 + }
3085 +
3086 + TEST_METHOD(PortMappingNat)
3087 + {
3088 + ValidatePortMapping(WSLCNetworkingModeNAT);
3089 + }
3090 +
3091 + TEST_METHOD(PortMappingVirtioProxy)
3092 + {
3093 + ValidatePortMapping(WSLCNetworkingModeVirtioProxy);
3094 + }
3095 +
3096 + WSLC_TEST_METHOD(StuckVmTermination)
3097 + {
3098 + // Create a 'stuck' process
3099 + auto process = WSLCProcessLauncher{"/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin}.Launch(*m_defaultSession);
3100 +
3101 + // Stop the service
3102 + StopWslService();
3103 +
3104 + ResetTestSession(); // Reopen the session since the service was stopped.
3105 + }
3106 +
3107 + void ValidateWindowsMounts(bool enableVirtioFs)
3108 + {
3109 + auto settings = GetDefaultSessionSettings(L"windows-mount-tests");
3110 + WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs, enableVirtioFs);
3111 +
3112 + // Reuse the default session if possible.
3113 + auto createNewSession = enableVirtioFs != WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3114 + auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
3115 +
3116 + auto expectedMountOptions = [&](bool readOnly) -> std::string {
3117 + if (enableVirtioFs)
3118 + {
3119 + return std::format("/win-path*virtiofs*{},relatime*", readOnly ? "ro" : "rw");
3120 + }
3121 + else
3122 + {
3123 + return std::format(
3124 + "/win-path*9p*{},relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*", readOnly ? "ro" : "rw");
3125 + }
3126 + };
3127 +
3128 + auto testFolder = std::filesystem::current_path() / "test-folder";
3129 + std::filesystem::create_directories(testFolder);
3130 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testFolder); });
3131 +
3132 + // Validate writeable mount.
3133 + {
3134 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3135 + ExpectMount(session.get(), "/win-path", expectedMountOptions(false));
3136 +
3137 + // Validate that mount can't be stacked on each other
3138 + VERIFY_ARE_EQUAL(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3139 +
3140 + // Validate that folder is writeable from linux
3141 + ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt && sync"}, 0);
3142 + VERIFY_ARE_EQUAL(ReadFileContent(testFolder / "file.txt"), L"content");
3143 +
3144 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3145 + ExpectMount(session.get(), "/win-path", {});
3146 + }
3147 +
3148 + // Validate read-only mount.
3149 + {
3150 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3151 + ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3152 +
3153 + // Validate that folder is not writeable from linux
3154 + ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
3155 +
3156 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3157 + ExpectMount(session.get(), "/win-path", {});
3158 + }
3159 +
3160 + // Validate that a read-only share cannot be made writeable via mount -o remount,rw.
3161 + {
3162 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3163 + ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3164 +
3165 + // Attempt an in-place remount to read-write from the guest.
3166 + ExpectCommandResult(session.get(), {"/bin/sh", "-c", "mount -o remount,rw /win-path"}, 0);
3167 +
3168 + // Verify the folder is still not writeable.
3169 + ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
3170 +
3171 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3172 + ExpectMount(session.get(), "/win-path", {});
3173 + }
3174 +
3175 + // Validate that the device host enforces read-only even if the guest tries to bypass mount options.
3176 + if (enableVirtioFs)
3177 + {
3178 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3179 + ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3180 +
3181 + // Capture the mount source and type, unmount, then remount without read-only.
3182 + ExpectCommandResult(
3183 + session.get(),
3184 + {"/bin/sh",
3185 + "-c",
3186 + "src=$(findmnt -n -o SOURCE /win-path) && "
3187 + "fstype=$(findmnt -n -o FSTYPE /win-path) && "
3188 + "umount /win-path && "
3189 + "mount -t $fstype $src /win-path"},
3190 + 0);
3191 +
3192 + // Verify the folder is still not writeable.
3193 + ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
3194 +
3195 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3196 + ExpectMount(session.get(), "/win-path", {});
3197 + }
3198 +
3199 + // Validate various error paths
3200 + {
3201 + VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"relative-path", "/win-path", true), E_INVALIDARG);
3202 + VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"C:\\does-not-exist", "/win-path", true), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
3203 + VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/not-mounted"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3204 + VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/proc"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3205 +
3206 + // Validate that folders that are manually unmounted from the guest are handled properly
3207 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3208 + ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3209 +
3210 + ExpectCommandResult(session.get(), {"/usr/bin/umount", "/win-path"}, 0);
3211 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3212 + }
3213 + }
3214 +
3215 + WSLC_TEST_METHOD(WindowsMounts)
3216 + {
3217 + ValidateWindowsMounts(false);
3218 + }
3219 +
3220 + WSLC_TEST_METHOD(WindowsMountsVirtioFs)
3221 + {
3222 + ValidateWindowsMounts(true);
3223 + }
3224 +
3225 + // Validates that VirtioFs shares are reused across mount/unmount cycles for the same Windows folder.
3226 + WSLC_TEST_METHOD(WindowsMountsVirtioFsShareReuse)
3227 + {
3228 + auto settings = GetDefaultSessionSettings(L"virtiofs-share-reuse-test");
3229 + WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3230 +
3231 + auto createNewSession = !WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3232 + auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
3233 +
3234 + auto testFolder = std::filesystem::current_path() / "test-folder-share-reuse";
3235 + std::filesystem::create_directories(testFolder);
3236 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testFolder); });
3237 +
3238 + auto getMountSource = [&](const char* mountPoint) -> std::string {
3239 + auto cmd = std::format("findmnt -n -o SOURCE {}", mountPoint);
3240 + auto result = ExpectCommandResult(session.get(), {"/bin/sh", "-c", cmd}, 0);
3241 + return result.Output[1];
3242 + };
3243 +
3244 + // Mount, capture the source (share GUID), unmount, remount, verify same GUID is reused.
3245 + {
3246 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3247 + auto firstSource = getMountSource("/win-path");
3248 + VERIFY_IS_FALSE(firstSource.empty());
3249 +
3250 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3251 + ExpectMount(session.get(), "/win-path", {});
3252 +
3253 + // Remount the same folder - should reuse the same share GUID.
3254 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3255 + auto secondSource = getMountSource("/win-path");
3256 +
3257 + VERIFY_ARE_EQUAL(firstSource, secondSource);
3258 +
3259 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3260 + }
3261 +
3262 + // Verify that changing the read-only flag produces a different share GUID.
3263 + {
3264 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3265 + auto rwSource = getMountSource("/win-path");
3266 +
3267 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3268 +
3269 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3270 + auto roSource = getMountSource("/win-path");
3271 +
3272 + VERIFY_ARE_NOT_EQUAL(rwSource, roSource);
3273 +
3274 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3275 + }
3276 + }
3277 +
3278 + // This test case validates that no file descriptors are leaked to user processes.
3279 + WSLC_TEST_METHOD(Fd)
3280 + {
3281 + auto result = ExpectCommandResult(
3282 + m_defaultSession.get(), {"/bin/sh", "-c", "echo /proc/self/fd/* && (readlink -v /proc/self/fd/* || true)"}, 0);
3283 +
3284 + // Note: fd/0 is opened by readlink to read the actual content of /proc/self/fd.
3285 + if (!PathMatchSpecA(result.Output[1].c_str(), "/proc/self/fd/0 /proc/self/fd/1 /proc/self/fd/2\nsocket:*\nsocket:*"))
3286 + {
3287 + LogInfo("Found additional fds: %hs", result.Output[1].c_str());
3288 + VERIFY_FAIL();
3289 + }
3290 + }
3291 +
3292 + WSLC_TEST_METHOD(GPU)
3293 + {
3294 + // Validate that trying to mount the shares without GPU support enabled fails.
3295 + {
3296 + auto settings = GetDefaultSessionSettings(L"gpu-test-disabled");
3297 + WI_ClearFlag(settings.FeatureFlags, WslcFeatureFlagsGPU);
3298 +
3299 + auto createNewSession = WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsGPU);
3300 + auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
3301 +
3302 + // Validate that the GPU device is not available.
3303 + ExpectMount(session.get(), "/usr/lib/wsl/drivers", {});
3304 + ExpectMount(session.get(), "/usr/lib/wsl/lib", {});
3305 + }
3306 +
3307 + // Validate that the GPU device is available when enabled.
3308 + {
3309 + auto settings = GetDefaultSessionSettings(L"gpu-test");
3310 + WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsGPU);
3311 +
3312 + auto createNewSession = !WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsGPU);
3313 + auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
3314 +
3315 + // Validate that the GPU device is available.
3316 + ExpectCommandResult(session.get(), {"/bin/sh", "-c", "test -c /dev/dxg"}, 0);
3317 +
3318 + ExpectMount(
3319 + session.get(),
3320 + "/usr/lib/wsl/drivers",
3321 + "/usr/lib/wsl/drivers*9p*relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*");
3322 +
3323 + ExpectMount(
3324 + session.get(),
3325 + "/usr/lib/wsl/lib",
3326 + "/usr/lib/wsl/lib none*overlay ro,relatime,lowerdir=/usr/lib/wsl/lib/packaged*");
3327 +
3328 + // Validate that the mount points are not writeable.
3329 + VERIFY_ARE_EQUAL(RunCommand(session.get(), {"/usr/bin/touch", "/usr/lib/wsl/drivers/test"}).Code, 1L);
3330 + VERIFY_ARE_EQUAL(RunCommand(session.get(), {"/usr/bin/touch", "/usr/lib/wsl/lib/test"}).Code, 1L);
3331 + }
3332 + }
3333 +
3334 + WSLC_TEST_METHOD(Modules)
3335 + {
3336 + // Sanity check.
3337 + ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "lsmod | grep ^xsk_diag"}, 1);
3338 +
3339 + // Validate that modules can be loaded.
3340 + ExpectCommandResult(m_defaultSession.get(), {"/usr/sbin/modprobe", "xsk_diag"}, 0);
3341 +
3342 + // Validate that xsk_diag is now loaded.
3343 + ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "lsmod | grep ^xsk_diag"}, 0);
3344 + }
3345 +
3346 + WSLC_TEST_METHOD(CreateRootNamespaceProcess)
3347 + {
3348 + // Simple case
3349 + {
3350 + auto result = ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "echo OK"}, 0);
3351 + VERIFY_ARE_EQUAL(result.Output[1], "OK\n");
3352 + VERIFY_ARE_EQUAL(result.Output[2], "");
3353 + }
3354 +
3355 + // Stdout + stderr
3356 + {
3357 +
3358 + auto result = ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "echo stdout && (echo stderr 1>& 2)"}, 0);
3359 + VERIFY_ARE_EQUAL(result.Output[1], "stdout\n");
3360 + VERIFY_ARE_EQUAL(result.Output[2], "stderr\n");
3361 + }
3362 +
3363 + // Write a large stdin buffer and expect it back on stdout.
3364 + {
3365 + std::vector<char> largeBuffer;
3366 + std::string pattern = "ExpectedBufferContent";
3367 +
3368 + for (size_t i = 0; i < 1024 * 1024; i++)
3369 + {
3370 + largeBuffer.insert(largeBuffer.end(), pattern.begin(), pattern.end());
3371 + }
3372 +
3373 + WSLCProcessLauncher launcher("/bin/sh", {"/bin/sh", "-c", "cat && (echo completed 1>& 2)"}, {}, WSLCProcessFlagsStdin);
3374 +
3375 + auto process = launcher.Launch(*m_defaultSession);
3376 +
3377 + std::unique_ptr<OverlappedIOHandle> writeStdin(new WriteHandle(process.GetStdHandle(0), largeBuffer));
3378 + std::vector<std::unique_ptr<OverlappedIOHandle>> extraHandles;
3379 + extraHandles.emplace_back(std::move(writeStdin));
3380 +
3381 + auto result = process.WaitAndCaptureOutput(INFINITE, std::move(extraHandles));
3382 +
3383 + VERIFY_IS_TRUE(std::equal(largeBuffer.begin(), largeBuffer.end(), result.Output[1].begin(), result.Output[1].end()));
3384 + VERIFY_ARE_EQUAL(result.Output[2], "completed\n");
3385 +
3386 + // Validate that a null out handle is rejected.
3387 +
3388 + VERIFY_ARE_EQUAL(process.Get().GetStdHandle(WSLCFDStdout, nullptr), HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER));
3389 + }
3390 +
3391 + // Create a stuck process and kill it.
3392 + {
3393 + WSLCProcessLauncher launcher("/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin);
3394 +
3395 + auto process = launcher.Launch(*m_defaultSession);
3396 +
3397 + // Try to send invalid signal to the process
3398 + VERIFY_ARE_EQUAL(process.Get().Signal(9999), E_FAIL);
3399 +
3400 + // Send SIGKILL(9) to the process.
3401 + VERIFY_SUCCEEDED(process.Get().Signal(WSLCSignalSIGKILL));
3402 +
3403 + auto result = process.WaitAndCaptureOutput();
3404 + VERIFY_ARE_EQUAL(result.Code, WSLCSignalSIGKILL + 128);
3405 + VERIFY_ARE_EQUAL(result.Output[1], "");
3406 + VERIFY_ARE_EQUAL(result.Output[2], "");
3407 +
3408 + // Validate that process can't be signalled after it exited.
3409 + VERIFY_ARE_EQUAL(process.Get().Signal(WSLCSignalSIGKILL), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
3410 + }
3411 +
3412 + // Validate that errno is correctly propagated
3413 + {
3414 + WSLCProcessLauncher launcher("doesnotexist", {});
3415 +
3416 + auto [hresult, process, error] = launcher.LaunchNoThrow(*m_defaultSession);
3417 + VERIFY_ARE_EQUAL(hresult, E_FAIL);
3418 + VERIFY_ARE_EQUAL(error, 2); // ENOENT
3419 + VERIFY_IS_FALSE(process.has_value());
3420 + }
3421 +
3422 + {
3423 + WSLCProcessLauncher launcher("/", {});
3424 +
3425 + auto [hresult, process, error] = launcher.LaunchNoThrow(*m_defaultSession);
3426 + VERIFY_ARE_EQUAL(hresult, E_FAIL);
3427 + VERIFY_ARE_EQUAL(error, 13); // EACCESS
3428 + VERIFY_IS_FALSE(process.has_value());
3429 + }
3430 +
3431 + {
3432 + WSLCProcessLauncher launcher("/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin);
3433 +
3434 + auto process = launcher.Launch(*m_defaultSession);
3435 + auto stdoutHandle = process.GetStdHandle(1);
3436 +
3437 + COMOutputHandle dummyHandle;
3438 + // Verify that the same handle can only be acquired once.
3439 + VERIFY_ARE_EQUAL(process.Get().GetStdHandle(WSLCFDStdout, &dummyHandle), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
3440 +
3441 + // Verify that trying to acquire a std handle that doesn't exist fails as expected.
3442 + VERIFY_ARE_EQUAL(process.Get().GetStdHandle(static_cast<WSLCFD>(3), &dummyHandle), E_INVALIDARG);
3443 +
3444 + // Validate that the process object correctly handle requests after the VM has terminated.
3445 + ResetTestSession();
3446 + VERIFY_ARE_EQUAL(process.Get().Signal(WSLCSignalSIGKILL), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
3447 + }
3448 +
3449 + // Validate that empty arguments are correctly handled.
3450 + {
3451 + WSLCProcessLauncher launcher({"/usr/bin/echo"}, {"/usr/bin/echo", "foo", "", "bar"});
3452 +
3453 + auto process = launcher.Launch(*m_defaultSession);
3454 + ValidateProcessOutput(process, {{1, "foo bar\n"}}); // expect two spaces for the empty argument.
3455 + }
3456 +
3457 + // Validate error paths
3458 + {
3459 + WSLCProcessLauncher launcher("/bin/bash", {"/bin/bash"});
3460 + launcher.SetUser("nobody"); // Custom users are not supported for root namespace processes.
3461 +
3462 + auto [hresult, error, process] = launcher.LaunchNoThrow(*m_defaultSession);
3463 + VERIFY_ARE_EQUAL(hresult, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
3464 + }
3465 + }
3466 +
3467 + WSLC_TEST_METHOD(CrashDumpCollection)
3468 + {
3469 + int processId = 0;
3470 +
3471 + // Cache the existing crash dumps so we can check that a new one is created.
3472 + auto crashDumpsDir = std::filesystem::temp_directory_path() / "wslc-crashes";
3473 + std::set<std::filesystem::path> existingDumps;
3474 +
3475 + if (std::filesystem::exists(crashDumpsDir))
3476 + {
3477 + existingDumps = {std::filesystem::directory_iterator(crashDumpsDir), std::filesystem::directory_iterator{}};
3478 + }
3479 +
3480 + // Create a stuck process and crash it.
3481 + {
3482 + WSLCProcessLauncher launcher("/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin);
3483 +
3484 + auto process = launcher.Launch(*m_defaultSession);
3485 +
3486 + // Get the process id. This is need to identify the crash dump file.
3487 + VERIFY_SUCCEEDED(process.Get().GetPid(&processId));
3488 +
3489 + // Send SIGSEV(11) to crash the process.
3490 + VERIFY_SUCCEEDED(process.Get().Signal(WSLCSignalSIGSEGV));
3491 +
3492 + auto result = process.WaitAndCaptureOutput();
3493 + VERIFY_ARE_EQUAL(result.Code, 128 + WSLCSignalSIGSEGV);
3494 + VERIFY_ARE_EQUAL(result.Output[1], "");
3495 + VERIFY_ARE_EQUAL(result.Output[2], "");
3496 +
3497 + VERIFY_ARE_EQUAL(process.Get().Signal(WSLCSignalSIGKILL), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
3498 + }
3499 +
3500 + // Dumps files are named with the format: wsl-crash-<sessionId>-<pid>-<processname>-<code>.dmp
3501 + // Check if a new file was added in crashDumpsDir matching the pattern and not in existingDumps.
3502 + std::string expectedPattern = std::format("wsl-crash-*-{}-_usr_bin_cat-11.dmp", processId);
3503 +
3504 + auto dumpFile = wsl::shared::retry::RetryWithTimeout<std::filesystem::path>(
3505 + [crashDumpsDir, expectedPattern, existingDumps]() {
3506 + for (const auto& entry : std::filesystem::directory_iterator(crashDumpsDir))
3507 + {
3508 + const auto& filePath = entry.path();
3509 + if (existingDumps.find(filePath) == existingDumps.end() &&
3510 + PathMatchSpecA(filePath.filename().string().c_str(), expectedPattern.c_str()))
3511 + {
3512 + return filePath;
3513 + }
3514 + }
3515 +
3516 + throw wil::ResultException(HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3517 + },
3518 + std::chrono::milliseconds{100},
3519 + std::chrono::seconds{10});
3520 +
3521 + // Ensure that the dump file is cleaned up after test completion.
3522 + auto cleanup = wil::scope_exit([&] {
3523 + if (std::filesystem::exists(dumpFile))
3524 + {
3525 + std::filesystem::remove(dumpFile);
3526 + }
3527 + });
3528 +
3529 + VERIFY_IS_TRUE(std::filesystem::exists(dumpFile));
3530 + VERIFY_IS_TRUE(std::filesystem::file_size(dumpFile) > 0);
3531 + }
3532 +
3533 + WSLC_TEST_METHOD(VhdFormatting)
3534 + {
3535 + constexpr auto formatedVhd = L"test-format-vhd.vhdx";
3536 +
3537 + // TODO: Replace this by a proper SDK method once it exists
3538 + auto tokenInfo = wil::get_token_information<TOKEN_USER>();
3539 + wsl::core::filesystem::CreateVhd(formatedVhd, 100 * 1024 * 1024, tokenInfo->User.Sid, false, false);
3540 +
3541 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(formatedVhd)); });
3542 +
3543 + // Format the disk.
3544 + auto absoluteVhdPath = std::filesystem::absolute(formatedVhd).wstring();
3545 + VERIFY_SUCCEEDED(m_defaultSession->FormatVirtualDisk(absoluteVhdPath.c_str()));
3546 +
3547 + // Validate error paths.
3548 + VERIFY_ARE_EQUAL(m_defaultSession->FormatVirtualDisk(L"DoesNotExist.vhdx"), E_INVALIDARG);
3549 + VERIFY_ARE_EQUAL(m_defaultSession->FormatVirtualDisk(L"C:\\DoesNotExist.vhdx"), HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
3550 + }
3551 +
3552 + // Exercises behavior that all volume drivers must implement identically:
3553 + // create, duplicate-name rejection, multi-mount, cross-container read/write,
3554 + // in-use deletion rejection, and clean deletion after the referencing container is removed.
3555 + void ValidateNamedVolumeContract(std::string_view driver, const WSLCDriverOption* driverOpts, ULONG driverOptsCount)
3556 + {
3557 + const std::string driverStr(driver);
3558 + const std::string volumeName = std::format("wslc-test-named-volume-{}", driver);
3559 +
3560 + // Best-effort cleanup in case of leftovers from a previous failed run.
3561 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3562 +
3563 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
3564 +
3565 + WSLCVolumeOptions volumeOptions{};
3566 + volumeOptions.Name = volumeName.c_str();
3567 + volumeOptions.Driver = driverStr.c_str();
3568 + volumeOptions.DriverOpts = driverOpts;
3569 + volumeOptions.DriverOptsCount = driverOptsCount;
3570 +
3571 + // Create volume and validate duplicate volume name handling.
3572 + WSLCVolumeInformation volInfo{};
3573 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3574 + VERIFY_ARE_EQUAL(std::string(volInfo.Name), volumeName);
3575 + VERIFY_ARE_EQUAL(std::string(volInfo.Driver), driverStr);
3576 + VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&volumeOptions, &volInfo), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3577 +
3578 + // Verify the same named volume can be mounted more than once with different container paths.
3579 + {
3580 + WSLCContainerLauncher duplicateNamedVolumes(
3581 + "debian:latest",
3582 + std::format("named-volume-dup-{}", driver),
3583 + {"/bin/sh", "-c", "echo duplicated >/data-a/dup.txt ; cat /data-b/dup.txt"});
3584 + duplicateNamedVolumes.AddNamedVolume(volumeName, "/data-a", false);
3585 + duplicateNamedVolumes.AddNamedVolume(volumeName, "/data-b", true);
3586 +
3587 + auto duplicateNamedVolumesContainer = duplicateNamedVolumes.Launch(*m_defaultSession);
3588 + auto duplicateNamedVolumesProcess = duplicateNamedVolumesContainer.GetInitProcess();
3589 + ValidateProcessOutput(duplicateNamedVolumesProcess, {{1, "duplicated\n"}});
3590 + }
3591 +
3592 + // Verify CreateContainer with named volume mounts the volume into the container.
3593 + {
3594 + WSLCContainerLauncher writer(
3595 + "debian:latest",
3596 + std::format("named-volume-writer-{}", driver),
3597 + {"/bin/sh", "-c", "echo wslc-named-volume >/data/marker.txt"});
3598 + writer.AddNamedVolume(volumeName, "/data", false);
3599 +
3600 + auto writerContainer = writer.Launch(*m_defaultSession);
3601 + auto writerProcess = writerContainer.GetInitProcess();
3602 + ValidateProcessOutput(writerProcess, {});
3603 +
3604 + WSLCContainerLauncher reader(
3605 + "debian:latest", std::format("named-volume-reader-{}", driver), {"/bin/sh", "-c", "cat /data/marker.txt"});
3606 + reader.AddNamedVolume(volumeName, "/data", true);
3607 +
3608 + auto readerContainer = reader.Launch(*m_defaultSession);
3609 + auto readerProcess = readerContainer.GetInitProcess();
3610 + ValidateProcessOutput(readerProcess, {{1, "wslc-named-volume\n"}});
3611 + }
3612 +
3613 + // Verify we cannot delete a named volume while a container references it.
3614 + WSLCContainerLauncher holder("debian:latest", std::format("named-volume-holder-{}", driver), {"sleep", "99999"});
3615 + holder.AddNamedVolume(volumeName, "/data", false);
3616 +
3617 + auto [holderCreateResult, holderContainerResult] = holder.CreateNoThrow(*m_defaultSession);
3618 + VERIFY_SUCCEEDED(holderCreateResult);
3619 + VERIFY_IS_TRUE(holderContainerResult.has_value());
3620 +
3621 + auto holderContainer = std::move(holderContainerResult.value());
3622 + holderContainer.SetDeleteOnClose(false);
3623 +
3624 + VERIFY_ARE_EQUAL(m_defaultSession->DeleteVolume(volumeName.c_str()), HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION));
3625 +
3626 + // Verify that after deleting the container, the volume can be deleted.
3627 + VERIFY_SUCCEEDED(holderContainer.Get().Delete(WSLCDeleteFlagsNone));
3628 + VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3629 +
3630 + cleanup.release();
3631 + }
3632 +
3633 + WSLC_TEST_METHOD(NamedVolumesVhd)
3634 + {
3635 + WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}};
3636 + ValidateNamedVolumeContract("vhd", driverOpts, ARRAYSIZE(driverOpts));
3637 +
3638 + // VHD-driver-specific: validate the host-side .vhdx artifact and the
3639 + // /mnt/wslc-volumes ext4 mount inside the VM appear and disappear with
3640 + // the volume.
3641 + const std::string volumeName = "wslc-test-named-volume-vhd-host";
3642 + const std::filesystem::path volumeVhdPath = m_storagePath / "volumes" / (volumeName + ".vhdx");
3643 +
3644 + WSLCVolumeOptions volumeOptions{};
3645 + volumeOptions.Name = volumeName.c_str();
3646 + volumeOptions.Driver = "vhd";
3647 + volumeOptions.DriverOpts = driverOpts;
3648 + volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
3649 +
3650 + WSLCVolumeInformation volInfo{};
3651 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3652 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
3653 +
3654 + VERIFY_IS_TRUE(std::filesystem::exists(volumeVhdPath));
3655 + ExpectMount(m_defaultSession.get(), std::format("/mnt/wslc-volumes/{}", volumeName), std::optional<std::string>{"*ext4*"});
3656 +
3657 + VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3658 + cleanup.release();
3659 +
3660 + ExpectMount(m_defaultSession.get(), std::format("/mnt/wslc-volumes/{}", volumeName), std::nullopt);
3661 + VERIFY_IS_FALSE(std::filesystem::exists(volumeVhdPath));
3662 + }
3663 +
3664 + WSLC_TEST_METHOD(NamedVolumesGuest)
3665 + {
3666 + ValidateNamedVolumeContract("guest", nullptr, 0);
3667 + }
3668 +
3669 + // Verifies that a container using a named volume survives a session restart and the volume's data is preserved.
3670 + void ValidateNamedVolumeRecoveryContract(std::string_view driver, const WSLCDriverOption* driverOpts, ULONG driverOptsCount)
3671 + {
3672 + const std::string driverStr(driver);
3673 + const std::string volumeName = std::format("wslc-test-named-volume-{}", driver);
3674 + const std::string containerName = std::format("wslc-test-container-{}", driver);
3675 +
3676 + // Best-effort cleanup in case prior failed runs left artifacts behind.
3677 + RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "rm", "-f", containerName});
3678 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3679 +
3680 + auto cleanup = wil::scope_exit([&]() {
3681 + RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "rm", "-f", containerName});
3682 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3683 + });
3684 +
3685 + WSLCVolumeOptions volumeOptions{};
3686 + volumeOptions.Name = volumeName.c_str();
3687 + volumeOptions.Driver = driverStr.c_str();
3688 + volumeOptions.DriverOpts = driverOpts;
3689 + volumeOptions.DriverOptsCount = driverOptsCount;
3690 +
3691 + WSLCVolumeInformation volInfo{};
3692 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3693 +
3694 + // Create a container that uses the named volume and writes a marker.
3695 + {
3696 + WSLCContainerLauncher writer(
3697 + "debian:latest", containerName, {"/bin/sh", "-c", "echo named-volume-recovery >/data/marker.txt"});
3698 + writer.AddNamedVolume(volumeName, "/data", false);
3699 +
3700 + auto writerContainer = writer.Launch(*m_defaultSession);
3701 + writerContainer.SetDeleteOnClose(false);
3702 +
3703 + auto writerProcess = writerContainer.GetInitProcess();
3704 + ValidateProcessOutput(writerProcess, {});
3705 + }
3706 +
3707 + // Restart the session and verify the container is recovered.
3708 + ResetTestSession();
3709 +
3710 + auto recoveredContainer = OpenContainer(m_defaultSession.get(), containerName);
3711 + recoveredContainer.SetDeleteOnClose(false);
3712 +
3713 + // Verify the named volume still contains the marker after restart.
3714 + {
3715 + WSLCContainerLauncher reader(
3716 + "debian:latest", std::format("{}-reader", containerName), {"/bin/sh", "-c", "cat /data/marker.txt"});
3717 + reader.AddNamedVolume(volumeName, "/data", true);
3718 +
3719 + auto readerContainer = reader.Launch(*m_defaultSession);
3720 + auto readerProcess = readerContainer.GetInitProcess();
3721 + ValidateProcessOutput(readerProcess, {{1, "named-volume-recovery\n"}});
3722 + }
3723 + }
3724 +
3725 + WSLC_TEST_METHOD(NamedVolumeRecovery)
3726 + {
3727 + ValidateNamedVolumeRecoveryContract("guest", nullptr, 0);
3728 + }
3729 +
3730 + WSLC_TEST_METHOD(NamedVolumesVhdSessionRecovery)
3731 + {
3732 +
3733 + WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}};
3734 + ValidateNamedVolumeRecoveryContract("vhd", driverOpts, ARRAYSIZE(driverOpts));
3735 +
3736 + // Re-create the volume (the recovery helper cleans up on exit) so we
3737 + // can test the "delete VHD while session is down" scenario.
3738 + const std::string volumeName = "wslc-test-named-volume-vhd";
3739 + const std::string containerName = "wslc-test-container-vhd";
3740 +
3741 + // Prune containers on exit so this test doesn't leak "wslc-test-container-vhd" on exit.
3742 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
3743 + PruneResult result;
3744 + LOG_IF_FAILED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result));
3745 + });
3746 +
3747 + WSLCVolumeOptions volumeOptions{};
3748 + volumeOptions.Name = volumeName.c_str();
3749 + volumeOptions.Driver = "vhd";
3750 + volumeOptions.DriverOpts = driverOpts;
3751 + volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
3752 +
3753 + WSLCVolumeInformation volInfo{};
3754 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3755 +
3756 + // Create a container that depends on the volume so we can verify it
3757 + // gets dropped when the backing .vhdx is removed.
3758 + {
3759 + WSLCContainerLauncher writer("debian:latest", containerName, {"/bin/sh", "-c", "echo vhd-recovery >/data/marker.txt"});
3760 + writer.AddNamedVolume(volumeName, "/data", false);
3761 +
3762 + auto writerContainer = writer.Launch(*m_defaultSession);
3763 + writerContainer.SetDeleteOnClose(false);
3764 +
3765 + auto writerProcess = writerContainer.GetInitProcess();
3766 + ValidateProcessOutput(writerProcess, {});
3767 + }
3768 +
3769 + const std::filesystem::path volumeVhdPath = m_storagePath / "volumes" / (volumeName + ".vhdx");
3770 +
3771 + {
3772 + auto restartSession = ResetTestSession();
3773 +
3774 + VERIFY_IS_TRUE(std::filesystem::exists(volumeVhdPath));
3775 +
3776 + std::error_code error;
3777 + VERIFY_IS_TRUE(std::filesystem::remove(volumeVhdPath, error));
3778 + VERIFY_ARE_EQUAL(error, std::error_code{});
3779 + }
3780 +
3781 + wil::com_ptr<IWSLCContainer> notFound;
3782 + VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer(containerName.c_str(), &notFound), E_UNEXPECTED);
3783 +
3784 + // Deleting the named volume should fail since the volume was not recovered.
3785 + VERIFY_ARE_EQUAL(m_defaultSession->DeleteVolume(volumeName.c_str()), WSLC_E_VOLUME_NOT_FOUND);
3786 + }
3787 +
3788 + WSLC_TEST_METHOD(NamedVolumeGuestDriverOptsTest)
3789 + {
3790 + const std::string volumeName = "wslc-test-vol";
3791 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3792 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
3793 +
3794 + auto expectReject = [&](const WSLCDriverOption* opts, ULONG optsCount, const std::wstring& expectedMessage) {
3795 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3796 +
3797 + WSLCVolumeOptions volumeOptions{};
3798 + volumeOptions.Name = volumeName.c_str();
3799 + volumeOptions.Driver = "guest";
3800 + volumeOptions.DriverOpts = opts;
3801 + volumeOptions.DriverOptsCount = optsCount;
3802 +
3803 + WSLCVolumeInformation volInfo{};
3804 + VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&volumeOptions, &volInfo), E_INVALIDARG);
3805 + ValidateCOMErrorMessageContains(expectedMessage);
3806 + };
3807 +
3808 + auto expectAccept = [&](const WSLCDriverOption* opts, ULONG optsCount) {
3809 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3810 +
3811 + WSLCVolumeOptions volumeOptions{};
3812 + volumeOptions.Name = volumeName.c_str();
3813 + volumeOptions.Driver = "guest";
3814 + volumeOptions.DriverOpts = opts;
3815 + volumeOptions.DriverOptsCount = optsCount;
3816 +
3817 + WSLCVolumeInformation volInfo{};
3818 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3819 + };
3820 +
3821 + // Allowed: no options (nullptr).
3822 + expectAccept(nullptr, 0);
3823 +
3824 + // Allowed: type=tmpfs with device=tmpfs.
3825 + {
3826 + WSLCDriverOption opts[] = {{"type", "tmpfs"}, {"device", "tmpfs"}};
3827 + expectAccept(opts, ARRAYSIZE(opts));
3828 + }
3829 +
3830 + // Allowed: type=tmpfs with device=tmpfs and o= suboptions.
3831 + {
3832 + WSLCDriverOption opts[] = {{"type", "tmpfs"}, {"device", "tmpfs"}, {"o", "size=100m,uid=1000"}};
3833 + expectAccept(opts, ARRAYSIZE(opts));
3834 + }
3835 +
3836 + // Blocked: type=none (bind mount).
3837 + {
3838 + WSLCDriverOption opts[] = {{"type", "none"}};
3839 + expectReject(opts, ARRAYSIZE(opts), L"unsupported volume driver options: type=none");
3840 + }
3841 +
3842 + // Blocked: type=nfs.
3843 + {
3844 + WSLCDriverOption opts[] = {{"type", "nfs"}};
3845 + expectReject(opts, ARRAYSIZE(opts), L"unsupported volume driver options: type=nfs");
3846 + }
3847 +
3848 + // Blocked by Docker: device without type.
3849 + {
3850 + WSLCDriverOption opts[] = {{"device", "/some/path"}};
3851 + expectReject(opts, ARRAYSIZE(opts), L"create wslc-test-vol: missing required option: \"type\"");
3852 + }
3853 +
3854 + // Blocked by Docker: device=tmpfs without type.
3855 + {
3856 + WSLCDriverOption opts[] = {{"device", "tmpfs"}};
3857 + expectReject(opts, ARRAYSIZE(opts), L"create wslc-test-vol: missing required option: \"type\"");
3858 + }
3859 +
3860 + // Blocked by Docker: device and o without type.
3861 + {
3862 + WSLCDriverOption opts[] = {{"device", "tmpfs"}, {"o", "size=100m"}};
3863 + expectReject(opts, ARRAYSIZE(opts), L"create wslc-test-vol: missing required option: \"type\"");
3864 + }
3865 + }
3866 +
3867 + WSLC_TEST_METHOD(NamedVolumeVhdOptionsParseTest)
3868 + {
3869 + const std::string volumeName = "wslc-volume-name";
3870 +
3871 + auto validateInvalidOptionsFailure = [&](const WSLCDriverOption* opts,
3872 + ULONG optsCount,
3873 + HRESULT expectedResult,
3874 + const std::optional<std::wstring>& expectedMessage = std::nullopt) {
3875 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3876 +
3877 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
3878 +
3879 + WSLCVolumeOptions volumeOptions{};
3880 + volumeOptions.Name = volumeName.c_str();
3881 + volumeOptions.Driver = "vhd";
3882 + volumeOptions.DriverOpts = opts;
3883 + volumeOptions.DriverOptsCount = optsCount;
3884 +
3885 + WSLCVolumeInformation volInfo{};
3886 + const auto result = m_defaultSession->CreateVolume(&volumeOptions, &volInfo);
3887 +
3888 + if (result != expectedResult)
3889 + {
3890 + LogInfo("CreateVolume mismatch result=0x%08x expected=0x%08x", static_cast<unsigned int>(result), static_cast<unsigned int>(expectedResult));
3891 + }
3892 +
3893 + VERIFY_ARE_EQUAL(result, expectedResult);
3894 + if (expectedMessage.has_value())
3895 + {
3896 + ValidateCOMErrorMessage(expectedMessage);
3897 + }
3898 + };
3899 +
3900 + // Missing SizeBytes.
3901 + validateInvalidOptionsFailure(nullptr, 0, E_INVALIDARG, L"Missing required option: 'SizeBytes'");
3902 +
3903 + WSLCDriverOption wrongOption[] = {{"WrongOption", "value"}};
3904 + validateInvalidOptionsFailure(wrongOption, ARRAYSIZE(wrongOption), E_INVALIDARG, L"Missing required option: 'SizeBytes'");
3905 +
3906 + // Invalid SizeBytes values.
3907 + WSLCDriverOption emptySize[] = {{"SizeBytes", ""}};
3908 + validateInvalidOptionsFailure(emptySize, ARRAYSIZE(emptySize), E_INVALIDARG, L"Invalid size: ");
3909 +
3910 + WSLCDriverOption zeroSize[] = {{"SizeBytes", "0"}};
3911 + validateInvalidOptionsFailure(zeroSize, ARRAYSIZE(zeroSize), E_INVALIDARG, L"Invalid size: 0");
3912 +
3913 + WSLCDriverOption invalidSizeAbc[] = {{"SizeBytes", "abc"}};
3914 + validateInvalidOptionsFailure(invalidSizeAbc, ARRAYSIZE(invalidSizeAbc), E_INVALIDARG, L"Invalid size: abc");
3915 +
3916 + WSLCDriverOption invalidSizeMixed[] = {{"SizeBytes", "123abc"}};
3917 + validateInvalidOptionsFailure(invalidSizeMixed, ARRAYSIZE(invalidSizeMixed), E_INVALIDARG, L"Invalid size: 123abc");
3918 +
3919 + WSLCDriverOption invalidSizeSign[] = {{"SizeBytes", "+-1"}};
3920 + validateInvalidOptionsFailure(invalidSizeSign, ARRAYSIZE(invalidSizeSign), E_INVALIDARG, L"Invalid size: +-1");
3921 +
3922 + WSLCDriverOption invalidSizeOverflow[] = {{"SizeBytes", "18446744073709551616"}};
3923 + validateInvalidOptionsFailure(
3924 + invalidSizeOverflow, ARRAYSIZE(invalidSizeOverflow), E_INVALIDARG, L"Invalid size: 18446744073709551616");
3925 +
3926 + WSLCDriverOption invalidSizeNeg[] = {{"SizeBytes", "-1"}};
3927 + validateInvalidOptionsFailure(invalidSizeNeg, ARRAYSIZE(invalidSizeNeg), E_INVALIDARG, L"Invalid size: -1");
3928 + }
3929 +
3930 + WSLC_TEST_METHOD(ListAndInspectNamedVolumesTest)
3931 + {
3932 + const std::string vhdVolumeName = "wsla-test-vol-vhd";
3933 + const std::string guestVolumeName = "wsla-test-vol-guest";
3934 +
3935 + auto cleanup = wil::scope_exit([&]() {
3936 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(vhdVolumeName.c_str()));
3937 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(guestVolumeName.c_str()));
3938 + });
3939 +
3940 + // Verify empty list is returned when no volumes exist.
3941 + wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
3942 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
3943 + VERIFY_ARE_EQUAL(0u, volumes.size());
3944 +
3945 + // Create a VHD volume and verify list returns one entry.
3946 + WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}};
3947 +
3948 + WSLCVolumeOptions vhdOptions{};
3949 + vhdOptions.Name = vhdVolumeName.c_str();
3950 + vhdOptions.Driver = "vhd";
3951 + vhdOptions.DriverOpts = driverOpts;
3952 + vhdOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
3953 +
3954 + WSLCVolumeInformation volInfo{};
3955 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&vhdOptions, &volInfo));
3956 +
3957 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
3958 + VERIFY_ARE_EQUAL(1u, volumes.size());
3959 + VERIFY_ARE_EQUAL(std::string(volumes[0].Name), vhdVolumeName);
3960 + VERIFY_ARE_EQUAL(std::string(volumes[0].Driver), std::string("vhd"));
3961 +
3962 + // Verify that a guest volume cannot be created with the same name as an existing vhd volume.
3963 + WSLCVolumeOptions duplicateGuestOptions{};
3964 + duplicateGuestOptions.Name = vhdVolumeName.c_str();
3965 + duplicateGuestOptions.Driver = "guest";
3966 + VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&duplicateGuestOptions, &volInfo), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3967 +
3968 + // Create a guest volume and verify both drivers show up in the list.
3969 + WSLCVolumeOptions guestOptions{};
3970 + guestOptions.Name = guestVolumeName.c_str();
3971 + guestOptions.Driver = "guest";
3972 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&guestOptions, &volInfo));
3973 +
3974 + // Verify that a vhd volume cannot be created with the same name as an existing guest volume.
3975 + WSLCVolumeOptions duplicateVhdOptions{};
3976 + duplicateVhdOptions.Name = guestVolumeName.c_str();
3977 + duplicateVhdOptions.Driver = "vhd";
3978 + duplicateVhdOptions.DriverOpts = driverOpts;
3979 + duplicateVhdOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
3980 + VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&duplicateVhdOptions, &volInfo), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3981 +
3982 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
3983 + VERIFY_ARE_EQUAL(2u, volumes.size());
3984 +
3985 + std::map<std::string, std::string> namesToDrivers;
3986 + for (const auto& v : volumes)
3987 + {
3988 + namesToDrivers.emplace(v.Name, v.Driver);
3989 + }
3990 +
3991 + VERIFY_ARE_EQUAL(namesToDrivers[vhdVolumeName], std::string("vhd"));
3992 + VERIFY_ARE_EQUAL(namesToDrivers[guestVolumeName], std::string("guest"));
3993 +
3994 + // Verify InspectVolume returns correct details for the VHD volume (driver opts present).
3995 + wil::unique_cotaskmem_ansistring output;
3996 + VERIFY_SUCCEEDED(m_defaultSession->InspectVolume(vhdVolumeName.c_str(), &output));
3997 + VERIFY_IS_NOT_NULL(output.get());
3998 +
3999 + auto vhdInspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectVolume>(output.get());
4000 + VERIFY_ARE_EQUAL(vhdInspect.Name, vhdVolumeName);
4001 + VERIFY_ARE_EQUAL(vhdInspect.Driver, std::string("vhd"));
4002 + VERIFY_IS_TRUE(vhdInspect.DriverOpts.contains("SizeBytes"));
4003 +
4004 + // Verify InspectVolume returns correct details for the guest volume (no driver opts).
4005 + output.reset();
4006 + VERIFY_SUCCEEDED(m_defaultSession->InspectVolume(guestVolumeName.c_str(), &output));
4007 + VERIFY_IS_NOT_NULL(output.get());
4008 +
4009 + auto guestInspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectVolume>(output.get());
4010 + VERIFY_ARE_EQUAL(guestInspect.Name, guestVolumeName);
4011 + VERIFY_ARE_EQUAL(guestInspect.Driver, std::string("guest"));
4012 + VERIFY_IS_TRUE(guestInspect.DriverOpts.empty());
4013 +
4014 + // Verify InspectVolume fails for a non-existent volume.
4015 + output.reset();
4016 + VERIFY_ARE_EQUAL(m_defaultSession->InspectVolume("does-not-exist", &output), WSLC_E_VOLUME_NOT_FOUND);
4017 +
4018 + // Delete the VHD volume and verify only the guest volume remains.
4019 + VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(vhdVolumeName.c_str()));
4020 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
4021 + VERIFY_ARE_EQUAL(1u, volumes.size());
4022 + VERIFY_ARE_EQUAL(std::string(volumes[0].Name), guestVolumeName);
4023 + VERIFY_ARE_EQUAL(std::string(volumes[0].Driver), std::string("guest"));
4024 + }
4025 +
4026 + WSLC_TEST_METHOD(NetworkCreateDeleteListTest)
4027 + {
4028 + const std::string networkName = "test-network";
4029 +
4030 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4031 +
4032 + // List should start empty.
4033 + wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4034 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4035 + VERIFY_ARE_EQUAL(0u, networks.size());
4036 +
4037 + WSLCNetworkOptions options{};
4038 + options.Name = networkName.c_str();
4039 + options.Driver = "bridge";
4040 + options.DriverOpts = nullptr;
4041 + options.DriverOptsCount = 0;
4042 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4043 +
4044 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4045 +
4046 + // Verify it appears in the list with correct fields.
4047 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4048 + VERIFY_ARE_EQUAL(1u, networks.size());
4049 + VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4050 + VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver));
4051 + VERIFY_IS_TRUE(strlen(networks[0].Id) > 0);
4052 +
4053 + // Duplicate name should fail.
4054 + VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_defaultSession->CreateNetwork(&options));
4055 +
4056 + cleanup.release();
4057 + VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4058 +
4059 + // List should be empty again.
4060 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4061 + VERIFY_ARE_EQUAL(0u, networks.size());
4062 +
4063 + // Delete non-existent should fail.
4064 + VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, m_defaultSession->DeleteNetwork(networkName.c_str()));
4065 + }
4066 +
4067 + WSLC_TEST_METHOD(NetworkCreateWithSubnetTest)
4068 + {
4069 + const std::string networkName = "subnet-test-net";
4070 +
4071 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4072 +
4073 + WSLCDriverOption subnetOpt[] = {{"Subnet", "172.28.0.0/16"}};
4074 +
4075 + WSLCNetworkOptions options{};
4076 + options.Name = networkName.c_str();
4077 + options.Driver = "bridge";
4078 + options.DriverOpts = subnetOpt;
4079 + options.DriverOptsCount = ARRAYSIZE(subnetOpt);
4080 +
4081 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4082 +
4083 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4084 +
4085 + wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4086 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4087 + VERIFY_ARE_EQUAL(1u, networks.size());
4088 + VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4089 + }
4090 +
4091 + WSLC_TEST_METHOD(NetworkCreateInternalTest)
4092 + {
4093 + const std::string networkName = "internal-test-net";
4094 +
4095 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4096 +
4097 + WSLCDriverOption internalOpt[] = {{"Internal", "true"}};
4098 +
4099 + WSLCNetworkOptions options{};
4100 + options.Name = networkName.c_str();
4101 + options.Driver = "bridge";
4102 + options.DriverOpts = internalOpt;
4103 + options.DriverOptsCount = ARRAYSIZE(internalOpt);
4104 +
4105 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4106 +
4107 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4108 +
4109 + wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4110 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4111 + VERIFY_ARE_EQUAL(1u, networks.size());
4112 + VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4113 + }
4114 +
4115 + WSLC_TEST_METHOD(NetworkCreateWithLabelsTest)
4116 + {
4117 + const std::string networkName = "labels-test-net";
4118 +
4119 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4120 +
4121 + WSLCLabel labels[] = {
4122 + {.Key = "com.example.env", .Value = "test"},
4123 + {.Key = "com.example.team", .Value = "infra"},
4124 + };
4125 +
4126 + WSLCNetworkOptions options{};
4127 + options.Name = networkName.c_str();
4128 + options.Driver = "bridge";
4129 + options.DriverOpts = nullptr;
4130 + options.DriverOptsCount = 0;
4131 + options.Labels = labels;
4132 + options.LabelsCount = ARRAYSIZE(labels);
4133 +
4134 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4135 +
4136 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4137 +
4138 + wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4139 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4140 + VERIFY_ARE_EQUAL(1u, networks.size());
4141 + VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4142 + }
4143 +
4144 + WSLC_TEST_METHOD(NetworkCreateInvalidDriverTest)
4145 + {
4146 + WSLCNetworkOptions options{};
4147 + options.Name = "bad-driver-net";
4148 + options.DriverOpts = nullptr;
4149 + options.DriverOptsCount = 0;
4150 +
4151 + for (const char* driver : {"overlay", "Bridge", ""})
4152 + {
4153 + options.Driver = driver;
4154 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4155 + ValidateCOMErrorMessageContains(L"Unsupported network driver:");
4156 + }
4157 + }
4158 +
4159 + WSLC_TEST_METHOD(NetworkCreateReservedNameTest)
4160 + {
4161 + WSLCNetworkOptions options{};
4162 + options.Driver = "bridge";
4163 + options.DriverOpts = nullptr;
4164 + options.DriverOptsCount = 0;
4165 +
4166 + options.Name = "bridge";
4167 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4168 + ValidateCOMErrorMessageContains(L"bridge");
4169 +
4170 + options.Name = "host";
4171 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4172 + ValidateCOMErrorMessageContains(L"host");
4173 +
4174 + options.Name = "none";
4175 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4176 + ValidateCOMErrorMessageContains(L"none");
4177 + }
4178 +
4179 + WSLC_TEST_METHOD(NetworkCreateInvalidNameTest)
4180 + {
4181 + WSLCNetworkOptions options{};
4182 + options.Name = "invalid name!";
4183 + options.Driver = "bridge";
4184 + options.DriverOpts = nullptr;
4185 + options.DriverOptsCount = 0;
4186 +
4187 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4188 + ValidateCOMErrorMessageContains(L"invalid name!");
4189 + }
4190 +
4191 + WSLC_TEST_METHOD(NetworkCreateInvalidSubnetTest)
4192 + {
4193 + const std::string networkName = "bad-subnet-net";
4194 +
4195 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4196 +
4197 + WSLCDriverOption opts[] = {{"Subnet", "not-a-cidr"}};
4198 +
4199 + WSLCNetworkOptions options{};
4200 + options.Name = networkName.c_str();
4201 + options.Driver = "bridge";
4202 + options.DriverOpts = opts;
4203 + options.DriverOptsCount = ARRAYSIZE(opts);
4204 +
4205 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4206 + ValidateCOMErrorMessageContains(L"invalid subnet");
4207 +
4208 + wil::unique_cotaskmem_ansistring output;
4209 + VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4210 + }
4211 +
4212 + WSLC_TEST_METHOD(NetworkCreateInvalidGatewayTest)
4213 + {
4214 + const std::string networkName = "bad-gateway-net";
4215 +
4216 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4217 +
4218 + WSLCDriverOption opts[] = {{"Subnet", "172.27.0.0/16"}, {"Gateway", "999.999.999.999"}};
4219 +
4220 + WSLCNetworkOptions options{};
4221 + options.Name = networkName.c_str();
4222 + options.Driver = "bridge";
4223 + options.DriverOpts = opts;
4224 + options.DriverOptsCount = ARRAYSIZE(opts);
4225 +
4226 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4227 + ValidateCOMErrorMessageContains(L"invalid gateway");
4228 +
4229 + wil::unique_cotaskmem_ansistring output;
4230 + VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4231 + }
4232 +
4233 + WSLC_TEST_METHOD(NetworkCreateWithGatewayTest)
4234 + {
4235 + const std::string networkName = "gateway-test-net";
4236 +
4237 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4238 +
4239 + WSLCDriverOption opts[] = {{"Subnet", "172.31.0.0/16"}, {"Gateway", "172.31.0.1"}};
4240 +
4241 + WSLCNetworkOptions options{};
4242 + options.Name = networkName.c_str();
4243 + options.Driver = "bridge";
4244 + options.DriverOpts = opts;
4245 + options.DriverOptsCount = ARRAYSIZE(opts);
4246 +
4247 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4248 +
4249 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4250 +
4251 + wil::unique_cotaskmem_ansistring output;
4252 + VERIFY_SUCCEEDED(m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4253 + VERIFY_IS_NOT_NULL(output.get());
4254 +
4255 + auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectNetwork>(output.get());
4256 + VERIFY_IS_TRUE(inspect.IPAM.Config.has_value());
4257 + VERIFY_ARE_EQUAL(1u, inspect.IPAM.Config->size());
4258 + VERIFY_ARE_EQUAL(std::string("172.31.0.0/16"), inspect.IPAM.Config->at(0).Subnet);
4259 + VERIFY_ARE_EQUAL(std::string("172.31.0.1"), inspect.IPAM.Config->at(0).Gateway);
4260 + }
4261 +
4262 + WSLC_TEST_METHOD(NetworkSessionRecoveryTest)
4263 + {
4264 + const std::string networkName = "recovery-test-net";
4265 +
4266 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4267 +
4268 + WSLCNetworkOptions options{};
4269 + options.Name = networkName.c_str();
4270 + options.Driver = "bridge";
4271 + options.DriverOpts = nullptr;
4272 + options.DriverOptsCount = 0;
4273 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4274 +
4275 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4276 +
4277 + // Reset the session (simulates session restart).
4278 + ResetTestSession();
4279 +
4280 + wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4281 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4282 + VERIFY_ARE_EQUAL(1u, networks.size());
4283 + VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4284 + VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver));
4285 + VERIFY_IS_TRUE(strlen(networks[0].Id) > 0);
4286 + }
4287 +
4288 + WSLC_TEST_METHOD(NetworkMultipleCreateListDeleteTest)
4289 + {
4290 + const std::string networkNameA = "net-a";
4291 + const std::string networkNameB = "net-b";
4292 + const std::string networkNameC = "net-c";
4293 +
4294 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameA.c_str()));
4295 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameB.c_str()));
4296 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameC.c_str()));
4297 +
4298 + auto cleanup = wil::scope_exit([&]() {
4299 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameA.c_str()));
4300 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameB.c_str()));
4301 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameC.c_str()));
4302 + });
4303 +
4304 + WSLCNetworkOptions optionsA{};
4305 + optionsA.Name = networkNameA.c_str();
4306 + optionsA.Driver = "bridge";
4307 + optionsA.DriverOpts = nullptr;
4308 + optionsA.DriverOptsCount = 0;
4309 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&optionsA));
4310 +
4311 + WSLCDriverOption subnetOpt[] = {{"Subnet", "172.29.0.0/16"}};
4312 + WSLCNetworkOptions optionsB{};
4313 + optionsB.Name = networkNameB.c_str();
4314 + optionsB.Driver = "bridge";
4315 + optionsB.DriverOpts = subnetOpt;
4316 + optionsB.DriverOptsCount = ARRAYSIZE(subnetOpt);
4317 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&optionsB));
4318 +
4319 + WSLCDriverOption internalOpt[] = {{"Internal", "true"}};
4320 + WSLCNetworkOptions optionsC{};
4321 + optionsC.Name = networkNameC.c_str();
4322 + optionsC.Driver = "bridge";
4323 + optionsC.DriverOpts = internalOpt;
4324 + optionsC.DriverOptsCount = ARRAYSIZE(internalOpt);
4325 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&optionsC));
4326 +
4327 + wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4328 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4329 + VERIFY_ARE_EQUAL(3u, networks.size());
4330 +
4331 + VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkNameB.c_str()));
4332 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4333 + VERIFY_ARE_EQUAL(2u, networks.size());
4334 + }
4335 +
4336 + WSLC_TEST_METHOD(NetworkInspectTest)
4337 + {
4338 + const std::string networkName = "test-inspect-network";
4339 +
4340 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4341 +
4342 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4343 +
4344 + WSLCNetworkOptions options{};
4345 + options.Name = networkName.c_str();
4346 + options.Driver = "bridge";
4347 + options.DriverOpts = nullptr;
4348 + options.DriverOptsCount = 0;
4349 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4350 +
4351 + wil::unique_cotaskmem_ansistring output;
4352 + VERIFY_SUCCEEDED(m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4353 + VERIFY_IS_NOT_NULL(output.get());
4354 +
4355 + auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectNetwork>(output.get());
4356 + VERIFY_ARE_EQUAL(inspect.Name, networkName);
4357 + VERIFY_ARE_EQUAL(inspect.Driver, std::string("bridge"));
4358 + VERIFY_IS_FALSE(inspect.Id.empty());
4359 + VERIFY_IS_FALSE(inspect.Internal);
4360 + }
4361 +
4362 + WSLC_TEST_METHOD(NetworkInspectWithSubnetTest)
4363 + {
4364 + const std::string networkName = "test-inspect-subnet-net";
4365 +
4366 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4367 +
4368 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4369 +
4370 + WSLCDriverOption subnetOpt[] = {{"Subnet", "172.30.0.0/16"}};
4371 +
4372 + WSLCNetworkOptions options{};
4373 + options.Name = networkName.c_str();
4374 + options.Driver = "bridge";
4375 + options.DriverOpts = subnetOpt;
4376 + options.DriverOptsCount = ARRAYSIZE(subnetOpt);
4377 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4378 +
4379 + wil::unique_cotaskmem_ansistring output;
4380 + VERIFY_SUCCEEDED(m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4381 + VERIFY_IS_NOT_NULL(output.get());
4382 +
4383 + auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectNetwork>(output.get());
4384 + VERIFY_ARE_EQUAL(inspect.Name, networkName);
4385 + VERIFY_ARE_EQUAL(inspect.Driver, std::string("bridge"));
4386 + VERIFY_IS_TRUE(inspect.IPAM.Config.has_value());
4387 + VERIFY_ARE_EQUAL(1u, inspect.IPAM.Config->size());
4388 + VERIFY_ARE_EQUAL(std::string("172.30.0.0/16"), inspect.IPAM.Config->at(0).Subnet);
4389 + }
4390 +
4391 + WSLC_TEST_METHOD(NetworkInspectNotFoundTest)
4392 + {
4393 + wil::unique_cotaskmem_ansistring output;
4394 + auto hr = m_defaultSession->InspectNetwork("nonexistent-network", &output);
4395 + VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, hr);
4396 + ValidateCOMErrorMessageContains(L"nonexistent-network");
4397 + }
4398 +
4399 + WSLC_TEST_METHOD(CreateContainer)
4400 + {
4401 + // Test a simple container start.
4402 + {
4403 + WSLCContainerLauncher launcher("debian:latest", "test-simple", {"echo", "OK"});
4404 + auto container = launcher.Launch(*m_defaultSession);
4405 + auto process = container.GetInitProcess();
4406 +
4407 + ValidateProcessOutput(process, {{1, "OK\n"}});
4408 +
4409 + // Validate that GetInitProcess fails with the process argument is null.
4410 + VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER), container.Get().GetInitProcess(nullptr));
4411 + }
4412 +
4413 + // Validate that env is correctly wired.
4414 + {
4415 + WSLCContainerLauncher launcher("debian:latest", "test-env", {"/bin/sh", "-c", "echo $testenv"}, {{"testenv=testvalue"}});
4416 + auto container = launcher.Launch(*m_defaultSession);
4417 + auto process = container.GetInitProcess();
4418 +
4419 + ValidateProcessOutput(process, {{1, "testvalue\n"}});
4420 + }
4421 +
4422 + // Validate that exit codes are correctly wired.
4423 + {
4424 + WSLCContainerLauncher launcher("debian:latest", "test-exit-code", {"/bin/sh", "-c", "exit 12"});
4425 + auto container = launcher.Launch(*m_defaultSession);
4426 + auto process = container.GetInitProcess();
4427 +
4428 + ValidateProcessOutput(process, {}, 12);
4429 + }
4430 +
4431 + // Validate that stdin is correctly wired
4432 + {
4433 + WSLCContainerLauncher launcher(
4434 + "debian:latest", "test-default-entrypoint", {"/bin/cat"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost, WSLCProcessFlagsStdin);
4435 +
4436 + auto container = launcher.Launch(*m_defaultSession);
4437 +
4438 + auto process = container.GetInitProcess();
4439 + auto input = process.GetStdHandle(0);
4440 +
4441 + std::string shellInput = "foo";
4442 + std::vector<char> inputBuffer{shellInput.begin(), shellInput.end()};
4443 +
4444 + std::unique_ptr<OverlappedIOHandle> writeStdin(new WriteHandle(std::move(input), inputBuffer));
4445 +
4446 + std::vector<std::unique_ptr<OverlappedIOHandle>> extraHandles;
4447 + extraHandles.emplace_back(std::move(writeStdin));
4448 +
4449 + auto result = process.WaitAndCaptureOutput(INFINITE, std::move(extraHandles));
4450 +
4451 + VERIFY_ARE_EQUAL(result.Output[2], "");
4452 + VERIFY_ARE_EQUAL(result.Output[1], "foo");
4453 + }
4454 +
4455 + // Validate that stdin behaves correctly if closed without any input.
4456 + {
4457 + WSLCContainerLauncher launcher("debian:latest", "test-stdin", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
4458 + auto container = launcher.Launch(*m_defaultSession);
4459 + auto process = container.GetInitProcess();
4460 + process.GetStdHandle(0); // Close stdin;
4461 +
4462 + ValidateProcessOutput(process, {{1, ""}});
4463 + }
4464 +
4465 + // Validate that the default stop signal is respected.
4466 + {
4467 + WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-1", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
4468 + launcher.SetDefaultStopSignal(WSLCSignalSIGHUP);
4469 + launcher.SetContainerFlags(WSLCContainerFlagsInit);
4470 +
4471 + auto container = launcher.Launch(*m_defaultSession);
4472 + auto process = container.GetInitProcess();
4473 +
4474 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalNone, 60));
4475 +
4476 + // Validate that the init process exited with the expected signal.
4477 + VERIFY_ARE_EQUAL(process.Wait(), WSLCSignalSIGHUP + 128);
4478 + }
4479 +
4480 + // Validate that the default stop signal can be overriden.
4481 + {
4482 + WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-2", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
4483 + launcher.SetDefaultStopSignal(WSLCSignalSIGHUP);
4484 + launcher.SetContainerFlags(WSLCContainerFlagsInit);
4485 +
4486 + auto container = launcher.Launch(*m_defaultSession);
4487 + auto process = container.GetInitProcess();
4488 +
4489 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 60));
4490 +
4491 + // Validate that the init process exited with the expected signal.
4492 + VERIFY_ARE_EQUAL(process.Wait(), WSLCSignalSIGKILL + 128);
4493 + }
4494 +
4495 + // Validate that entrypoint is respected.
4496 + {
4497 + WSLCContainerLauncher launcher("debian:latest", "test-entrypoint", {"OK"});
4498 + launcher.SetEntrypoint({"/bin/echo", "-n"});
4499 +
4500 + auto container = launcher.Launch(*m_defaultSession);
4501 + auto process = container.GetInitProcess();
4502 + ValidateProcessOutput(process, {{1, "OK"}});
4503 + }
4504 +
4505 + // Validate that the working directory is correctly wired.
4506 + {
4507 + WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-1", {"pwd"});
4508 + launcher.SetWorkingDirectory("/tmp");
4509 +
4510 + auto container = launcher.Launch(*m_defaultSession);
4511 + auto process = container.GetInitProcess();
4512 + ValidateProcessOutput(process, {{1, "/tmp\n"}});
4513 + }
4514 +
4515 + // Validate that the current directory is created if it doesn't exist.
4516 + {
4517 + WSLCContainerLauncher launcher("debian:latest", "test-bad-cwd", {"pwd"});
4518 + launcher.SetWorkingDirectory("/new-dir");
4519 +
4520 + auto container = launcher.Launch(*m_defaultSession);
4521 + auto process = container.GetInitProcess();
4522 +
4523 + ValidateProcessOutput(process, {{1, "/new-dir\n"}});
4524 + }
4525 +
4526 + // Validate that hostname and domainanme are correctly wired.
4527 + {
4528 + WSLCContainerLauncher launcher("debian:latest", "test-hostname", {"/bin/sh", "-c", "echo $(hostname).$(domainname)"});
4529 +
4530 + launcher.SetHostname("my-host-name");
4531 + launcher.SetDomainname("my-domain-name");
4532 +
4533 + auto container = launcher.Launch(*m_defaultSession);
4534 + auto process = container.GetInitProcess();
4535 + ValidateProcessOutput(process, {{1, "my-host-name.my-domain-name\n"}});
4536 + }
4537 +
4538 + // Validate that containers without DNS configuration use default DNS.
4539 + {
4540 + WSLCContainerLauncher launcher("debian:latest", "test-no-dns", {"/bin/grep", "-iF", "nameserver", "/etc/resolv.conf"});
4541 +
4542 + auto container = launcher.Launch(*m_defaultSession);
4543 + auto process = container.GetInitProcess();
4544 + ValidateProcessOutput(process, {}, 0);
4545 + }
4546 +
4547 + // Validate that custom DNS servers are correctly wired.
4548 + {
4549 + WSLCContainerLauncher launcher(
4550 + "debian:latest", "test-dns-custom", {"/bin/grep", "-iF", "nameserver 1.2.3.4", "/etc/resolv.conf"});
4551 +
4552 + launcher.SetDnsServers({"1.2.3.4"});
4553 +
4554 + auto container = launcher.Launch(*m_defaultSession);
4555 + auto process = container.GetInitProcess();
4556 + ValidateProcessOutput(process, {}, 0);
4557 + }
4558 +
4559 + // Validate that custom DNS search domains are correctly wired.
4560 + {
4561 + WSLCContainerLauncher launcher(
4562 + "debian:latest", "test-dns-search", {"/bin/grep", "-iF", "test.local", "/etc/resolv.conf"});
4563 +
4564 + launcher.SetDnsSearchDomains({"test.local"});
4565 +
4566 + auto container = launcher.Launch(*m_defaultSession);
4567 + auto process = container.GetInitProcess();
4568 + ValidateProcessOutput(process, {}, 0);
4569 + }
4570 +
4571 + // Validate that custom DNS options are correctly wired.
4572 + {
4573 + WSLCContainerLauncher launcher(
4574 + "debian:latest", "test-dns-options", {"/bin/grep", "-iF", "timeout:1", "/etc/resolv.conf"});
4575 +
4576 + launcher.SetDnsOptions({"timeout:1"});
4577 +
4578 + auto container = launcher.Launch(*m_defaultSession);
4579 + auto process = container.GetInitProcess();
4580 + ValidateProcessOutput(process, {}, 0);
4581 + }
4582 +
4583 + // Validate that multiple DNS options are correctly wired.
4584 + {
4585 + WSLCContainerLauncher launcher(
4586 + "debian:latest", "test-dns-options-multiple", {"/bin/grep", "-iF", "timeout:2", "/etc/resolv.conf"});
4587 +
4588 + launcher.SetDnsOptions({"timeout:1", "timeout:2"});
4589 +
4590 + auto container = launcher.Launch(*m_defaultSession);
4591 + auto process = container.GetInitProcess();
4592 + ValidateProcessOutput(process, {}, 0);
4593 + }
4594 +
4595 + // Validate that the username is correctly wired.
4596 + {
4597 + WSLCContainerLauncher launcher("debian:latest", "test-username", {"whoami"});
4598 +
4599 + launcher.SetUser("nobody");
4600 +
4601 + auto container = launcher.Launch(*m_defaultSession);
4602 + auto process = container.GetInitProcess();
4603 + ValidateProcessOutput(process, {{1, "nobody\n"}});
4604 + }
4605 +
4606 + // Validate that the group is correctly wired.
4607 + {
4608 + WSLCContainerLauncher launcher("debian:latest", "test-group", {"groups"});
4609 +
4610 + launcher.SetUser("nobody:www-data");
4611 +
4612 + auto container = launcher.Launch(*m_defaultSession);
4613 + auto process = container.GetInitProcess();
4614 + ValidateProcessOutput(process, {{1, "www-data\n"}});
4615 + }
4616 +
4617 + // Validate that the container behaves correctly if the caller keeps a reference to an init process during termination.
4618 + {
4619 + WSLCContainerLauncher launcher("debian:latest", "test-init-ref", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
4620 +
4621 + auto container = launcher.Launch(*m_defaultSession);
4622 + auto containerId = container.Id();
4623 +
4624 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
4625 + wil::com_ptr<IWSLCContainer> openedContainer;
4626 + VERIFY_SUCCEEDED(m_defaultSession->OpenContainer(containerId.c_str(), &openedContainer));
4627 + VERIFY_SUCCEEDED(openedContainer->Delete(WSLCDeleteFlagsNone));
4628 + });
4629 +
4630 + auto process = container.GetInitProcess();
4631 +
4632 + VERIFY_ARE_EQUAL(process.State(), WslcProcessStateRunning);
4633 +
4634 + // Terminate the session.
4635 + ResetTestSession();
4636 +
4637 + WSLCProcessState processState{};
4638 + int exitCode{};
4639 + VERIFY_ARE_EQUAL(process.Get().GetState(&processState, &exitCode), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
4640 +
4641 + WSLCContainerState state{};
4642 + VERIFY_ARE_EQUAL(container.Get().GetState(&state), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
4643 + }
4644 +
4645 + // Validate error handling when the username / group doesn't exist
4646 + {
4647 + WSLCContainerLauncher launcher("debian:latest", "test-no-missing-user", {"groups"});
4648 +
4649 + launcher.SetUser("does-not-exist");
4650 +
4651 + auto [result, _] = launcher.LaunchNoThrow(*m_defaultSession);
4652 + VERIFY_ARE_EQUAL(result, E_FAIL);
4653 +
4654 + ValidateCOMErrorMessage(L"unable to find user does-not-exist: no matching entries in passwd file");
4655 + }
4656 +
4657 + // Validate that empty arguments are correctly handled.
4658 + {
4659 + WSLCContainerLauncher launcher("debian:latest", "test-empty-args", {"echo", "foo", "", "bar"});
4660 +
4661 + auto container = launcher.Launch(*m_defaultSession);
4662 + auto process = container.GetInitProcess();
4663 + ValidateProcessOutput(process, {{1, "foo bar\n"}}); // Expect two spaces for the empty argument.
4664 + }
4665 +
4666 + // Validate that tmpfs mounts are correctly wired.
4667 + {
4668 + WSLCContainerLauncher launcher(
4669 + "debian:latest",
4670 + "test-tmpfs",
4671 + {"/bin/sh", "-c", "mount | grep 'tmpfs on /mnt/wslc-tmpfs1' && mount | grep 'tmpfs on /mnt/wslc-tmpfs2'"});
4672 +
4673 + launcher.AddTmpfs("/mnt/wslc-tmpfs1", "rw,noexec,nosuid,size=65536k");
4674 + launcher.AddTmpfs("/mnt/wslc-tmpfs2", "");
4675 +
4676 + auto container = launcher.Launch(*m_defaultSession);
4677 + auto process = container.GetInitProcess();
4678 + ValidateProcessOutput(process, {}, 0);
4679 + }
4680 +
4681 + // Validate that relative tmpfs paths are rejected by Docker.
4682 + {
4683 + WSLCContainerLauncher launcher("debian:latest", "test-tmpfs-relative", {"/bin/cat"});
4684 + launcher.AddTmpfs("relative-path", "");
4685 +
4686 + auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4687 + VERIFY_ARE_EQUAL(hresult, E_FAIL);
4688 +
4689 + ValidateCOMErrorMessage(L"invalid mount path: 'relative-path' mount path must be absolute");
4690 + }
4691 +
4692 + // Validate that invalid tmpfs options are rejected by Docker.
4693 + {
4694 + WSLCContainerLauncher launcher("debian:latest", "test-tmpfs-invalid-opts", {"/bin/cat"});
4695 + launcher.AddTmpfs("/mnt/wslc-tmpfs", "invalid_option_xyz");
4696 +
4697 + auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4698 + VERIFY_ARE_EQUAL(hresult, E_FAIL);
4699 +
4700 + ValidateCOMErrorMessage(L"invalid tmpfs option [\"invalid_option_xyz\"]");
4701 + }
4702 +
4703 + // Validate error paths
4704 + {
4705 + WSLCContainerLauncher launcher("debian:latest", std::string(WSLC_MAX_CONTAINER_NAME_LENGTH + 1, 'a'), {"/bin/cat"});
4706 + auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4707 + VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
4708 + }
4709 +
4710 + {
4711 + WSLCContainerLauncher launcher(std::string(WSLC_MAX_IMAGE_NAME_LENGTH + 1, 'a'), "dummy", {"/bin/cat"});
4712 + auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4713 + VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
4714 + }
4715 +
4716 + {
4717 + WSLCContainerLauncher launcher("invalid-image-name", "dummy", {"/bin/cat"});
4718 + auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4719 + VERIFY_ARE_EQUAL(hresult, WSLC_E_IMAGE_NOT_FOUND);
4720 + }
4721 +
4722 + {
4723 + WSLCContainerLauncher launcher("debian:latest", "dummy", {"/does-not-exist"});
4724 + auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4725 + VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
4726 +
4727 + ValidateCOMErrorMessage(
4728 + L"failed to create task for container: failed to create shim task: OCI runtime create failed: runc create "
4729 + L"failed: unable to start container process: error during container init: exec: \"/does-not-exist\": stat "
4730 + L"/does-not-exist: no such file or directory: unknown");
4731 + }
4732 +
4733 + // Test null image name
4734 + {
4735 + WSLCContainerOptions options{};
4736 + options.Image = nullptr;
4737 + options.Name = "test-container";
4738 + options.InitProcessOptions.CommandLine = {.Values = nullptr, .Count = 0};
4739 +
4740 + wil::com_ptr<IWSLCContainer> container;
4741 + auto hr = m_defaultSession->CreateContainer(&options, &container);
4742 + VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
4743 + }
4744 +
4745 + // Test null container name
4746 + {
4747 + WSLCContainerOptions options{};
4748 + options.Image = "debian:latest";
4749 + options.Name = nullptr;
4750 + options.InitProcessOptions.CommandLine = {.Values = nullptr, .Count = 0};
4751 +
4752 + wil::com_ptr<IWSLCContainer> container;
4753 + VERIFY_SUCCEEDED(m_defaultSession->CreateContainer(&options, &container));
4754 + VERIFY_SUCCEEDED(container->Delete(WSLCDeleteFlagsNone));
4755 + }
4756 + }
4757 +
4758 + WSLC_TEST_METHOD(ContainerStartAfterStop)
4759 + {
4760 + {
4761 + WSLCContainerLauncher launcher("debian:latest", "test-stop-start", {"echo", "OK"});
4762 + auto container = launcher.Launch(*m_defaultSession);
4763 + auto process = container.GetInitProcess();
4764 +
4765 + ValidateProcessOutput(process, {{1, "OK\n"}});
4766 +
4767 + {
4768 + // Validate that the container can be restarted.
4769 + VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsAttach, nullptr), S_OK);
4770 + auto restartedProcess = container.GetInitProcess();
4771 + ValidateProcessOutput(restartedProcess, {{1, "OK\n"}});
4772 + }
4773 +
4774 + {
4775 + // Validate that the container can be restarted without the attach flag.
4776 + VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr), S_OK);
4777 + auto restartedProcess = container.GetInitProcess();
4778 + VERIFY_ARE_EQUAL(restartedProcess.Wait(), 0);
4779 +
4780 + COMOutputHandle stdoutLogs{};
4781 + COMOutputHandle stderrLogs{};
4782 + VERIFY_SUCCEEDED(container.Get().Logs(WSLCLogsFlagsNone, &stdoutLogs, &stderrLogs, 0, 0, 0));
4783 +
4784 + ValidateHandleOutput(stdoutLogs.Get(), "OK\nOK\nOK\n");
4785 + ValidateHandleOutput(stderrLogs.Get(), "");
4786 + }
4787 + }
4788 +
4789 + // Validate that containers can be restarted after being explicitly stopped.
4790 + {
4791 + WSLCContainerLauncher launcher("debian:latest", "test-stop-start-2", {"sleep", "99999"});
4792 + auto container = launcher.Launch(*m_defaultSession);
4793 +
4794 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4795 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
4796 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
4797 +
4798 + VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
4799 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4800 +
4801 + auto initProcess = container.GetInitProcess();
4802 + initProcess.Get().Signal(WSLCSignalSIGKILL);
4803 + VERIFY_ARE_EQUAL(initProcess.Wait(), WSLCSignalSIGKILL + 128);
4804 +
4805 + VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
4806 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4807 +
4808 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
4809 + VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
4810 +
4811 + // Validate that deleted containers can't be started.
4812 + VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr), RPC_E_DISCONNECTED);
4813 + }
4814 +
4815 + // Validate restart behavior for a container with the autorm flag set
4816 + {
4817 + WSLCContainerLauncher launcher("debian:latest", "test-stop-start-3", {"sleep", "99999"});
4818 + launcher.SetContainerFlags(WSLCContainerFlagsRm);
4819 + auto container = launcher.Launch(*m_defaultSession);
4820 +
4821 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4822 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
4823 +
4824 + // Validate that deleted containers can't be started.
4825 + VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr), RPC_E_DISCONNECTED);
4826 + }
4827 +
4828 + // Validate that invalid start flags are rejected.
4829 + {
4830 + WSLCContainerLauncher launcher("debian:latest", "test-stop-start-invalid-flags", {"echo", "OK"});
4831 + auto container = launcher.Create(*m_defaultSession);
4832 + VERIFY_ARE_EQUAL(container.Get().Start(static_cast<WSLCContainerStartFlags>(0x2), nullptr), E_INVALIDARG);
4833 + }
4834 + }
4835 +
4836 + WSLC_TEST_METHOD(OpenContainer)
4837 + {
4838 + auto expectOpen = [&](const char* Id, HRESULT expectedResult = S_OK) {
4839 + wil::com_ptr<IWSLCContainer> container;
4840 + auto result = m_defaultSession->OpenContainer(Id, &container);
4841 +
4842 + VERIFY_ARE_EQUAL(result, expectedResult);
4843 +
4844 + return container;
4845 + };
4846 +
4847 + {
4848 + WSLCContainerLauncher launcher("debian:latest", "named-container", {"echo", "OK"});
4849 + auto [result, container] = launcher.CreateNoThrow(*m_defaultSession);
4850 + VERIFY_SUCCEEDED(result);
4851 +
4852 + VERIFY_ARE_EQUAL(container->Id().length(), WSLC_CONTAINER_ID_LENGTH);
4853 +
4854 + VERIFY_ARE_EQUAL(container->Name(), "named-container");
4855 +
4856 + // Validate that the container can be opened by name.
4857 + expectOpen("named-container");
4858 +
4859 + // Validate that the container can be opened by ID.
4860 + expectOpen(container->Id().c_str());
4861 +
4862 + // Validate that the container can be opened by a prefix of the ID.
4863 + expectOpen(container->Id().substr(0, 8).c_str());
4864 + expectOpen(container->Id().substr(0, 1).c_str());
4865 +
4866 + // Validate that prefix conflicts are correctly handled.
4867 + std::vector<RunningWSLCContainer> createdContainers;
4868 + createdContainers.emplace_back(std::move(container.value()));
4869 +
4870 + auto findConflict = [&]() {
4871 + for (auto& e : createdContainers)
4872 + {
4873 + auto firstChar = e.Id()[0];
4874 +
4875 + if (std::ranges::count_if(createdContainers, [&](auto& container) { return container.Id()[0] == firstChar; }) > 1)
4876 + {
4877 + return firstChar;
4878 + }
4879 + }
4880 +
4881 + return '\0';
4882 + };
4883 +
4884 + // Create containers until we get two containers with the same first character in their ID.
4885 + while (true)
4886 + {
4887 + VERIFY_IS_LESS_THAN(createdContainers.size(), 16);
4888 +
4889 + auto [result, newContainer] = WSLCContainerLauncher("debian:latest").CreateNoThrow(*m_defaultSession);
4890 + VERIFY_SUCCEEDED(result);
4891 +
4892 + createdContainers.emplace_back(std::move(newContainer.value()));
4893 + char conflictChar = findConflict();
4894 + if (conflictChar == '\0')
4895 + {
4896 + continue;
4897 + }
4898 +
4899 + expectOpen(std::string{&conflictChar, 1}.c_str(), WSLC_E_CONTAINER_PREFIX_AMBIGUOUS);
4900 + break;
4901 + }
4902 + }
4903 +
4904 + // Test error paths
4905 + {
4906 + expectOpen("", E_INVALIDARG);
4907 + ValidateCOMErrorMessage(L"Invalid name: ''");
4908 +
4909 + expectOpen("non-existing-container", WSLC_E_CONTAINER_NOT_FOUND);
4910 + ValidateCOMErrorMessage(L"Container 'non-existing-container' not found.");
4911 +
4912 + expectOpen("/", E_INVALIDARG);
4913 + ValidateCOMErrorMessage(L"Invalid name: '/'");
4914 +
4915 + expectOpen("?foo=bar", E_INVALIDARG);
4916 + ValidateCOMErrorMessage(L"Invalid name: '?foo=bar'");
4917 +
4918 + expectOpen("\n", E_INVALIDARG);
4919 + ValidateCOMErrorMessage(L"Invalid name: '\n'");
4920 +
4921 + expectOpen(" ", E_INVALIDARG);
4922 + ValidateCOMErrorMessage(L"Invalid name: ' '");
4923 + }
4924 + }
4925 +
4926 + WSLC_TEST_METHOD(ContainerState)
4927 + {
4928 + auto expectContainerList = [&](const std::vector<std::tuple<std::string, std::string, WSLCContainerState>>& expectedContainers) {
4929 + wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
4930 + wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
4931 +
4932 + VERIFY_SUCCEEDED(
4933 + m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
4934 + VERIFY_ARE_EQUAL(expectedContainers.size(), containers.size());
4935 +
4936 + for (size_t i = 0; i < expectedContainers.size(); i++)
4937 + {
4938 + const auto& [expectedName, expectedImage, expectedState] = expectedContainers[i];
4939 + VERIFY_ARE_EQUAL(expectedName, containers[i].Name);
4940 + VERIFY_ARE_EQUAL(expectedImage, containers[i].Image);
4941 + VERIFY_ARE_EQUAL(expectedState, containers[i].State);
4942 + VERIFY_ARE_EQUAL(strlen(containers[i].Id), WSLC_CONTAINER_ID_LENGTH);
4943 + VERIFY_IS_TRUE(containers[i].StateChangedAt > 0);
4944 + VERIFY_IS_TRUE(containers[i].CreatedAt > 0);
4945 + }
4946 + };
4947 +
4948 + {
4949 + // Validate that the container list is initially empty.
4950 + expectContainerList({});
4951 +
4952 + // Start one container and wait for it to exit.
4953 + {
4954 + WSLCContainerLauncher launcher("debian:latest", "exited-container", {"echo", "OK"});
4955 + auto container = launcher.Launch(*m_defaultSession);
4956 + auto process = container.GetInitProcess();
4957 +
4958 + ValidateProcessOutput(process, {{1, "OK\n"}});
4959 + expectContainerList({{"exited-container", "debian:latest", WslcContainerStateExited}});
4960 + }
4961 +
4962 + // Create a stuck container.
4963 + WSLCContainerLauncher launcher("debian:latest", "test-container-1", {"sleep", "99999"});
4964 +
4965 + auto container = launcher.Launch(*m_defaultSession);
4966 +
4967 + // Verify that the container is in running state.
4968 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4969 + expectContainerList({{"test-container-1", "debian:latest", WslcContainerStateRunning}});
4970 +
4971 + // Capture StateChangedAt and CreatedAt while the container is running.
4972 + ULONGLONG runningStateChangedAt{};
4973 + ULONGLONG runningCreatedAt{};
4974 + {
4975 + wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
4976 + wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
4977 + VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
4978 + &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
4979 + VERIFY_ARE_EQUAL(containers.size(), 1);
4980 + runningStateChangedAt = containers[0].StateChangedAt;
4981 + runningCreatedAt = containers[0].CreatedAt;
4982 + VERIFY_IS_TRUE(runningStateChangedAt > 0);
4983 + VERIFY_IS_TRUE(runningCreatedAt > 0);
4984 + }
4985 +
4986 + // Kill the container init process and expect it to be in exited state.
4987 + auto initProcess = container.GetInitProcess();
4988 + VERIFY_SUCCEEDED(initProcess.Get().Signal(WSLCSignalSIGKILL));
4989 +
4990 + // Wait for the process to actually exit.
4991 + wsl::shared::retry::RetryWithTimeout<void>(
4992 + [&]() {
4993 + initProcess.GetExitCode(); // Throw if the process hasn't exited yet.
4994 + },
4995 + std::chrono::milliseconds{100},
4996 + std::chrono::seconds{30});
4997 +
4998 + // Expect the container to be in exited state.
4999 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);

This file is too large to show in full.

test/windows/WindowsUpdateTests.cpp new
+1049
@@ -0,0 +1,1049 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WindowsUpdateTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WindowsUpdateIntegration.cpp.
12 + These tests use mock COM objects injected via the WindowsUpdateClassFactory
13 + abstraction so no real Windows Update service calls are made.
14 +
15 +--*/
16 +
17 +#include "precomp.h"
18 +#include "Common.h"
19 +#include "WindowsUpdateIntegration.h"
20 +
21 +using namespace wsl::windows::common;
22 +namespace WRL = Microsoft::WRL;
23 +
24 +namespace {
25 +
26 +// Stubs the 4 IDispatch pure virtual methods. All WUA interfaces derive from IDispatch.
27 +#define STUB_IDISPATCH() \
28 + STDMETHOD(GetTypeInfoCount)(UINT*) override \
29 + { \
30 + return E_NOTIMPL; \
31 + } \
32 + STDMETHOD(GetTypeInfo)(UINT, LCID, ITypeInfo**) override \
33 + { \
34 + return E_NOTIMPL; \
35 + } \
36 + STDMETHOD(GetIDsOfNames)(REFIID, LPOLESTR*, UINT, LCID, DISPID*) override \
37 + { \
38 + return E_NOTIMPL; \
39 + } \
40 + STDMETHOD(Invoke)(DISPID, REFIID, LCID, WORD, DISPPARAMS*, VARIANT*, EXCEPINFO*, UINT*) override \
41 + { \
42 + return E_NOTIMPL; \
43 + }
44 +
45 +// ---------------------------------------------------------------------------
46 +// MockUpdateCollection — implements IUpdateCollection
47 +// ---------------------------------------------------------------------------
48 +struct MockUpdateCollection : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IUpdateCollection>
49 +{
50 + STUB_IDISPATCH()
51 +
52 + std::vector<wil::com_ptr<IUpdate>> items;
53 + LONG addCallCount = 0;
54 +
55 + STDMETHOD(get_Count)(LONG* retval) override
56 + {
57 + *retval = static_cast<LONG>(items.size());
58 + return S_OK;
59 + }
60 +
61 + STDMETHOD(get_Item)(LONG index, IUpdate** retval) override
62 + {
63 + if (index < 0 || static_cast<size_t>(index) >= items.size())
64 + {
65 + return E_INVALIDARG;
66 + }
67 + *retval = items[index].get();
68 + (*retval)->AddRef();
69 + return S_OK;
70 + }
71 +
72 + STDMETHOD(Add)(IUpdate* value, LONG*) override
73 + {
74 + wil::com_ptr<IUpdate> u = value;
75 + items.push_back(std::move(u));
76 + ++addCallCount;
77 + return S_OK;
78 + }
79 +
80 + STDMETHOD(Clear)() override
81 + {
82 + items.clear();
83 + return S_OK;
84 + }
85 +
86 + STDMETHOD(put_Item)(LONG, IUpdate*) override
87 + {
88 + return E_NOTIMPL;
89 + }
90 + STDMETHOD(get__NewEnum)(IUnknown**) override
91 + {
92 + return E_NOTIMPL;
93 + }
94 + STDMETHOD(get_ReadOnly)(VARIANT_BOOL*) override
95 + {
96 + return E_NOTIMPL;
97 + }
98 + STDMETHOD(Copy)(IUpdateCollection**) override
99 + {
100 + return E_NOTIMPL;
101 + }
102 + STDMETHOD(Insert)(LONG, IUpdate*) override
103 + {
104 + return E_NOTIMPL;
105 + }
106 + STDMETHOD(RemoveAt)(LONG) override
107 + {
108 + return E_NOTIMPL;
109 + }
110 +};
111 +
112 +// ---------------------------------------------------------------------------
113 +// MockUpdate — implements IUpdate
114 +// ---------------------------------------------------------------------------
115 +struct MockUpdate : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IUpdate>
116 +{
117 + STUB_IDISPATCH()
118 +
119 + VARIANT_BOOL isDownloaded = VARIANT_FALSE;
120 +
121 + STDMETHOD(get_IsDownloaded)(VARIANT_BOOL* retval) override
122 + {
123 + *retval = isDownloaded;
124 + return S_OK;
125 + }
126 +
127 + STDMETHOD(get_Title)(BSTR*) override
128 + {
129 + return E_NOTIMPL;
130 + }
131 + STDMETHOD(get_AutoSelectOnWebSites)(VARIANT_BOOL*) override
132 + {
133 + return E_NOTIMPL;
134 + }
135 + STDMETHOD(get_BundledUpdates)(IUpdateCollection**) override
136 + {
137 + return E_NOTIMPL;
138 + }
139 + STDMETHOD(get_CanRequireSource)(VARIANT_BOOL*) override
140 + {
141 + return E_NOTIMPL;
142 + }
143 + STDMETHOD(get_Categories)(ICategoryCollection**) override
144 + {
145 + return E_NOTIMPL;
146 + }
147 + STDMETHOD(get_Deadline)(VARIANT*) override
148 + {
149 + return E_NOTIMPL;
150 + }
151 + STDMETHOD(get_DeltaCompressedContentAvailable)(VARIANT_BOOL*) override
152 + {
153 + return E_NOTIMPL;
154 + }
155 + STDMETHOD(get_DeltaCompressedContentPreferred)(VARIANT_BOOL*) override
156 + {
157 + return E_NOTIMPL;
158 + }
159 + STDMETHOD(get_Description)(BSTR*) override
160 + {
161 + return E_NOTIMPL;
162 + }
163 + STDMETHOD(get_EulaAccepted)(VARIANT_BOOL*) override
164 + {
165 + return E_NOTIMPL;
166 + }
167 + STDMETHOD(get_EulaText)(BSTR*) override
168 + {
169 + return E_NOTIMPL;
170 + }
171 + STDMETHOD(get_HandlerID)(BSTR*) override
172 + {
173 + return E_NOTIMPL;
174 + }
175 + STDMETHOD(get_Identity)(IUpdateIdentity**) override
176 + {
177 + return E_NOTIMPL;
178 + }
179 + STDMETHOD(get_Image)(IImageInformation**) override
180 + {
181 + return E_NOTIMPL;
182 + }
183 + STDMETHOD(get_InstallationBehavior)(IInstallationBehavior**) override
184 + {
185 + return E_NOTIMPL;
186 + }
187 + STDMETHOD(get_IsBeta)(VARIANT_BOOL*) override
188 + {
189 + return E_NOTIMPL;
190 + }
191 + STDMETHOD(get_IsHidden)(VARIANT_BOOL*) override
192 + {
193 + return E_NOTIMPL;
194 + }
195 + STDMETHOD(put_IsHidden)(VARIANT_BOOL) override
196 + {
197 + return E_NOTIMPL;
198 + }
199 + STDMETHOD(get_IsInstalled)(VARIANT_BOOL*) override
200 + {
201 + return E_NOTIMPL;
202 + }
203 + STDMETHOD(get_IsMandatory)(VARIANT_BOOL*) override
204 + {
205 + return E_NOTIMPL;
206 + }
207 + STDMETHOD(get_IsUninstallable)(VARIANT_BOOL*) override
208 + {
209 + return E_NOTIMPL;
210 + }
211 + STDMETHOD(get_Languages)(IStringCollection**) override
212 + {
213 + return E_NOTIMPL;
214 + }
215 + STDMETHOD(get_LastDeploymentChangeTime)(DATE*) override
216 + {
217 + return E_NOTIMPL;
218 + }
219 + STDMETHOD(get_MaxDownloadSize)(DECIMAL*) override
220 + {
221 + return E_NOTIMPL;
222 + }
223 + STDMETHOD(get_MinDownloadSize)(DECIMAL*) override
224 + {
225 + return E_NOTIMPL;
226 + }
227 + STDMETHOD(get_MoreInfoUrls)(IStringCollection**) override
228 + {
229 + return E_NOTIMPL;
230 + }
231 + STDMETHOD(get_MsrcSeverity)(BSTR*) override
232 + {
233 + return E_NOTIMPL;
234 + }
235 + STDMETHOD(get_RecommendedCpuSpeed)(LONG*) override
236 + {
237 + return E_NOTIMPL;
238 + }
239 + STDMETHOD(get_RecommendedHardDiskSpace)(LONG*) override
240 + {
241 + return E_NOTIMPL;
242 + }
243 + STDMETHOD(get_RecommendedMemory)(LONG*) override
244 + {
245 + return E_NOTIMPL;
246 + }
247 + STDMETHOD(get_ReleaseNotes)(BSTR*) override
248 + {
249 + return E_NOTIMPL;
250 + }
251 + STDMETHOD(get_SecurityBulletinIDs)(IStringCollection**) override
252 + {
253 + return E_NOTIMPL;
254 + }
255 + STDMETHOD(get_SupersededUpdateIDs)(IStringCollection**) override
256 + {
257 + return E_NOTIMPL;
258 + }
259 + STDMETHOD(get_SupportUrl)(BSTR*) override
260 + {
261 + return E_NOTIMPL;
262 + }
263 + STDMETHOD(get_Type)(UpdateType*) override
264 + {
265 + return E_NOTIMPL;
266 + }
267 + STDMETHOD(get_UninstallationNotes)(BSTR*) override
268 + {
269 + return E_NOTIMPL;
270 + }
271 + STDMETHOD(get_UninstallationBehavior)(IInstallationBehavior**) override
272 + {
273 + return E_NOTIMPL;
274 + }
275 + STDMETHOD(get_UninstallationSteps)(IStringCollection**) override
276 + {
277 + return E_NOTIMPL;
278 + }
279 + STDMETHOD(get_KBArticleIDs)(IStringCollection**) override
280 + {
281 + return E_NOTIMPL;
282 + }
283 + STDMETHOD(AcceptEula)() override
284 + {
285 + return E_NOTIMPL;
286 + }
287 + STDMETHOD(get_DeploymentAction)(DeploymentAction*) override
288 + {
289 + return E_NOTIMPL;
290 + }
291 + STDMETHOD(CopyFromCache)(BSTR, VARIANT_BOOL) override
292 + {
293 + return E_NOTIMPL;
294 + }
295 + STDMETHOD(get_DownloadPriority)(DownloadPriority*) override
296 + {
297 + return E_NOTIMPL;
298 + }
299 + STDMETHOD(get_DownloadContents)(IUpdateDownloadContentCollection**) override
300 + {
301 + return E_NOTIMPL;
302 + }
303 +};
304 +
305 +// ---------------------------------------------------------------------------
306 +// MockSearchResult — implements ISearchResult
307 +// ---------------------------------------------------------------------------
308 +struct MockSearchResult : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, ISearchResult>
309 +{
310 + STUB_IDISPATCH()
311 +
312 + OperationResultCode resultCode = OperationResultCode::orcSucceeded;
313 + wil::com_ptr_nothrow<MockUpdateCollection> updates = wil::MakeOrThrow<MockUpdateCollection>();
314 +
315 + STDMETHOD(get_ResultCode)(OperationResultCode* retval) override
316 + {
317 + *retval = resultCode;
318 + return S_OK;
319 + }
320 +
321 + STDMETHOD(get_Updates)(IUpdateCollection** retval) override
322 + {
323 + return updates.query_to(retval);
324 + }
325 +
326 + STDMETHOD(get_RootCategories)(ICategoryCollection**) override
327 + {
328 + return E_NOTIMPL;
329 + }
330 + STDMETHOD(get_Warnings)(IUpdateExceptionCollection**) override
331 + {
332 + return E_NOTIMPL;
333 + }
334 +};
335 +
336 +// ---------------------------------------------------------------------------
337 +// MockUpdateSearcher — implements IUpdateSearcher
338 +// ---------------------------------------------------------------------------
339 +struct MockUpdateSearcher : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IUpdateSearcher>
340 +{
341 + STUB_IDISPATCH()
342 +
343 + wil::com_ptr_nothrow<MockSearchResult> searchResult = wil::MakeOrThrow<MockSearchResult>();
344 +
345 + STDMETHOD(Search)(BSTR, ISearchResult** retval) override
346 + {
347 + return searchResult.query_to(retval);
348 + }
349 +
350 + STDMETHOD(get_CanAutomaticallyUpgradeService)(VARIANT_BOOL*) override
351 + {
352 + return E_NOTIMPL;
353 + }
354 + STDMETHOD(put_CanAutomaticallyUpgradeService)(VARIANT_BOOL) override
355 + {
356 + return E_NOTIMPL;
357 + }
358 + STDMETHOD(get_ClientApplicationID)(BSTR*) override
359 + {
360 + return E_NOTIMPL;
361 + }
362 + STDMETHOD(put_ClientApplicationID)(BSTR) override
363 + {
364 + return E_NOTIMPL;
365 + }
366 + STDMETHOD(get_IncludePotentiallySupersededUpdates)(VARIANT_BOOL*) override
367 + {
368 + return E_NOTIMPL;
369 + }
370 + STDMETHOD(put_IncludePotentiallySupersededUpdates)(VARIANT_BOOL) override
371 + {
372 + return E_NOTIMPL;
373 + }
374 + STDMETHOD(get_ServerSelection)(ServerSelection*) override
375 + {
376 + return E_NOTIMPL;
377 + }
378 + STDMETHOD(put_ServerSelection)(ServerSelection) override
379 + {
380 + return E_NOTIMPL;
381 + }
382 + STDMETHOD(BeginSearch)(BSTR, IUnknown*, VARIANT, ISearchJob**) override
383 + {
384 + return E_NOTIMPL;
385 + }
386 + STDMETHOD(EndSearch)(ISearchJob*, ISearchResult**) override
387 + {
388 + return E_NOTIMPL;
389 + }
390 + STDMETHOD(EscapeString)(BSTR, BSTR*) override
391 + {
392 + return E_NOTIMPL;
393 + }
394 + STDMETHOD(QueryHistory)(LONG, LONG, IUpdateHistoryEntryCollection**) override
395 + {
396 + return E_NOTIMPL;
397 + }
398 + STDMETHOD(get_Online)(VARIANT_BOOL*) override
399 + {
400 + return E_NOTIMPL;
401 + }
402 + STDMETHOD(put_Online)(VARIANT_BOOL) override
403 + {
404 + return E_NOTIMPL;
405 + }
406 + STDMETHOD(GetTotalHistoryCount)(LONG*) override
407 + {
408 + return E_NOTIMPL;
409 + }
410 + STDMETHOD(get_ServiceID)(BSTR*) override
411 + {
412 + return E_NOTIMPL;
413 + }
414 + STDMETHOD(put_ServiceID)(BSTR) override
415 + {
416 + return E_NOTIMPL;
417 + }
418 +};
419 +
420 +// ---------------------------------------------------------------------------
421 +// MockDownloadJob — implements IDownloadJob
422 +// ---------------------------------------------------------------------------
423 +struct MockDownloadJob : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IDownloadJob>
424 +{
425 + STUB_IDISPATCH()
426 +
427 + STDMETHOD(CleanUp)() override
428 + {
429 + return S_OK;
430 + }
431 + STDMETHOD(get_AsyncState)(VARIANT*) override
432 + {
433 + return E_NOTIMPL;
434 + }
435 + STDMETHOD(get_IsCompleted)(VARIANT_BOOL*) override
436 + {
437 + return E_NOTIMPL;
438 + }
439 + STDMETHOD(get_Updates)(IUpdateCollection**) override
440 + {
441 + return E_NOTIMPL;
442 + }
443 + STDMETHOD(GetProgress)(IDownloadProgress**) override
444 + {
445 + return E_NOTIMPL;
446 + }
447 + STDMETHOD(RequestAbort)() override
448 + {
449 + return E_NOTIMPL;
450 + }
451 +};
452 +
453 +// ---------------------------------------------------------------------------
454 +// MockDownloadResult — implements IDownloadResult
455 +// ---------------------------------------------------------------------------
456 +struct MockDownloadResult : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IDownloadResult>
457 +{
458 + STUB_IDISPATCH()
459 +
460 + HRESULT downloadHResult = S_OK;
461 +
462 + STDMETHOD(get_HResult)(HRESULT* retval) override
463 + {
464 + *retval = downloadHResult;
465 + return S_OK;
466 + }
467 +
468 + STDMETHOD(get_ResultCode)(OperationResultCode*) override
469 + {
470 + return E_NOTIMPL;
471 + }
472 + STDMETHOD(GetUpdateResult)(LONG, IUpdateDownloadResult**) override
473 + {
474 + return E_NOTIMPL;
475 + }
476 +};
477 +
478 +// ---------------------------------------------------------------------------
479 +// MockUpdateDownloader — implements IUpdateDownloader
480 +// ---------------------------------------------------------------------------
481 +struct MockUpdateDownloader : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IUpdateDownloader>
482 +{
483 + STUB_IDISPATCH()
484 +
485 + wil::com_ptr_nothrow<MockDownloadResult> downloadResult = wil::MakeOrThrow<MockDownloadResult>();
486 + wil::com_ptr<IUpdateCollection> capturedCollection;
487 + bool beginDownloadCalled = false;
488 +
489 + STDMETHOD(put_Updates)(IUpdateCollection* value) override
490 + {
491 + capturedCollection = value;
492 + return S_OK;
493 + }
494 +
495 + STDMETHOD(BeginDownload)(IUnknown*, IUnknown* completedCallback, VARIANT, IDownloadJob** retval) override
496 + {
497 + beginDownloadCalled = true;
498 + *retval = wil::MakeOrThrow<MockDownloadJob>().Detach();
499 + if (completedCallback)
500 + {
501 + wil::com_ptr<IDownloadCompletedCallback> cb;
502 + if (SUCCEEDED(completedCallback->QueryInterface(IID_PPV_ARGS(&cb))))
503 + {
504 + cb->Invoke(*retval, nullptr);
505 + }
506 + }
507 + return S_OK;
508 + }
509 +
510 + STDMETHOD(EndDownload)(IDownloadJob*, IDownloadResult** retval) override
511 + {
512 + return downloadResult.query_to(retval);
513 + }
514 +
515 + STDMETHOD(get_ClientApplicationID)(BSTR*) override
516 + {
517 + return E_NOTIMPL;
518 + }
519 + STDMETHOD(put_ClientApplicationID)(BSTR) override
520 + {
521 + return E_NOTIMPL;
522 + }
523 + STDMETHOD(get_IsForced)(VARIANT_BOOL*) override
524 + {
525 + return E_NOTIMPL;
526 + }
527 + STDMETHOD(put_IsForced)(VARIANT_BOOL) override
528 + {
529 + return E_NOTIMPL;
530 + }
531 + STDMETHOD(get_Priority)(DownloadPriority*) override
532 + {
533 + return E_NOTIMPL;
534 + }
535 + STDMETHOD(put_Priority)(DownloadPriority) override
536 + {
537 + return E_NOTIMPL;
538 + }
539 + STDMETHOD(get_Updates)(IUpdateCollection**) override
540 + {
541 + return E_NOTIMPL;
542 + }
543 + STDMETHOD(Download)(IDownloadResult**) override
544 + {
545 + return E_NOTIMPL;
546 + }
547 +};
548 +
549 +// ---------------------------------------------------------------------------
550 +// MockInstallationJob — implements IInstallationJob
551 +// ---------------------------------------------------------------------------
552 +struct MockInstallationJob : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IInstallationJob>
553 +{
554 + STUB_IDISPATCH()
555 +
556 + STDMETHOD(CleanUp)() override
557 + {
558 + return S_OK;
559 + }
560 + STDMETHOD(get_AsyncState)(VARIANT*) override
561 + {
562 + return E_NOTIMPL;
563 + }
564 + STDMETHOD(get_IsCompleted)(VARIANT_BOOL*) override
565 + {
566 + return E_NOTIMPL;
567 + }
568 + STDMETHOD(get_Updates)(IUpdateCollection**) override
569 + {
570 + return E_NOTIMPL;
571 + }
572 + STDMETHOD(GetProgress)(IInstallationProgress**) override
573 + {
574 + return E_NOTIMPL;
575 + }
576 + STDMETHOD(RequestAbort)() override
577 + {
578 + return E_NOTIMPL;
579 + }
580 +};
581 +
582 +// ---------------------------------------------------------------------------
583 +// MockInstallationResult — implements IInstallationResult
584 +// ---------------------------------------------------------------------------
585 +struct MockInstallationResult : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IInstallationResult>
586 +{
587 + STUB_IDISPATCH()
588 +
589 + HRESULT installHResult = S_OK;
590 +
591 + STDMETHOD(get_HResult)(HRESULT* retval) override
592 + {
593 + *retval = installHResult;
594 + return S_OK;
595 + }
596 +
597 + STDMETHOD(get_RebootRequired)(VARIANT_BOOL*) override
598 + {
599 + return E_NOTIMPL;
600 + }
601 + STDMETHOD(get_ResultCode)(OperationResultCode*) override
602 + {
603 + return E_NOTIMPL;
604 + }
605 + STDMETHOD(GetUpdateResult)(LONG, IUpdateInstallationResult**) override
606 + {
607 + return E_NOTIMPL;
608 + }
609 +};
610 +
611 +// ---------------------------------------------------------------------------
612 +// MockUpdateInstaller — implements IUpdateInstaller
613 +// ---------------------------------------------------------------------------
614 +struct MockUpdateInstaller : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IUpdateInstaller>
615 +{
616 + STUB_IDISPATCH()
617 +
618 + wil::com_ptr_nothrow<MockInstallationResult> installResult = wil::MakeOrThrow<MockInstallationResult>();
619 + wil::com_ptr<IUpdateCollection> capturedCollection;
620 + bool beginInstallCalled = false;
621 +
622 + STDMETHOD(put_Updates)(IUpdateCollection* value) override
623 + {
624 + capturedCollection = value;
625 + return S_OK;
626 + }
627 +
628 + STDMETHOD(BeginInstall)(IUnknown*, IUnknown* completedCallback, VARIANT, IInstallationJob** retval) override
629 + {
630 + beginInstallCalled = true;
631 + *retval = wil::MakeOrThrow<MockInstallationJob>().Detach();
632 + if (completedCallback)
633 + {
634 + wil::com_ptr<IInstallationCompletedCallback> cb;
635 + if (SUCCEEDED(completedCallback->QueryInterface(IID_PPV_ARGS(&cb))))
636 + {
637 + cb->Invoke(*retval, nullptr);
638 + }
639 + }
640 + return S_OK;
641 + }
642 +
643 + STDMETHOD(EndInstall)(IInstallationJob*, IInstallationResult** retval) override
644 + {
645 + return installResult.query_to(retval);
646 + }
647 +
648 + STDMETHOD(get_ClientApplicationID)(BSTR*) override
649 + {
650 + return E_NOTIMPL;
651 + }
652 + STDMETHOD(put_ClientApplicationID)(BSTR) override
653 + {
654 + return E_NOTIMPL;
655 + }
656 + STDMETHOD(get_IsForced)(VARIANT_BOOL*) override
657 + {
658 + return E_NOTIMPL;
659 + }
660 + STDMETHOD(put_IsForced)(VARIANT_BOOL) override
661 + {
662 + return E_NOTIMPL;
663 + }
664 + STDMETHOD(get_ParentHwnd)(HWND*) override
665 + {
666 + return E_NOTIMPL;
667 + }
668 + STDMETHOD(put_ParentHwnd)(HWND) override
669 + {
670 + return E_NOTIMPL;
671 + }
672 + STDMETHOD(put_ParentWindow)(IUnknown*) override
673 + {
674 + return E_NOTIMPL;
675 + }
676 + STDMETHOD(get_ParentWindow)(IUnknown**) override
677 + {
678 + return E_NOTIMPL;
679 + }
680 + STDMETHOD(get_Updates)(IUpdateCollection**) override
681 + {
682 + return E_NOTIMPL;
683 + }
684 + STDMETHOD(BeginUninstall)(IUnknown*, IUnknown*, VARIANT, IInstallationJob**) override
685 + {
686 + return E_NOTIMPL;
687 + }
688 + STDMETHOD(EndUninstall)(IInstallationJob*, IInstallationResult**) override
689 + {
690 + return E_NOTIMPL;
691 + }
692 + STDMETHOD(Install)(IInstallationResult**) override
693 + {
694 + return E_NOTIMPL;
695 + }
696 + STDMETHOD(RunWizard)(BSTR, IInstallationResult**) override
697 + {
698 + return E_NOTIMPL;
699 + }
700 + STDMETHOD(get_IsBusy)(VARIANT_BOOL*) override
701 + {
702 + return E_NOTIMPL;
703 + }
704 + STDMETHOD(Uninstall)(IInstallationResult**) override
705 + {
706 + return E_NOTIMPL;
707 + }
708 + STDMETHOD(get_AllowSourcePrompts)(VARIANT_BOOL*) override
709 + {
710 + return E_NOTIMPL;
711 + }
712 + STDMETHOD(put_AllowSourcePrompts)(VARIANT_BOOL) override
713 + {
714 + return E_NOTIMPL;
715 + }
716 + STDMETHOD(get_RebootRequiredBeforeInstallation)(VARIANT_BOOL*) override
717 + {
718 + return E_NOTIMPL;
719 + }
720 +};
721 +
722 +// ---------------------------------------------------------------------------
723 +// MockUpdateSession — implements IUpdateSession
724 +// ---------------------------------------------------------------------------
725 +struct MockUpdateSession : public WRL::RuntimeClass<WRL::RuntimeClassFlags<WRL::ClassicCom>, IUpdateSession>
726 +{
727 + STUB_IDISPATCH()
728 +
729 + wil::com_ptr_nothrow<MockUpdateSearcher> searcher = wil::MakeOrThrow<MockUpdateSearcher>();
730 + wil::com_ptr_nothrow<MockUpdateDownloader> downloader = wil::MakeOrThrow<MockUpdateDownloader>();
731 + wil::com_ptr_nothrow<MockUpdateInstaller> installer = wil::MakeOrThrow<MockUpdateInstaller>();
732 +
733 + STDMETHOD(put_ClientApplicationID)(BSTR) override
734 + {
735 + return S_OK;
736 + }
737 +
738 + STDMETHOD(CreateUpdateSearcher)(IUpdateSearcher** retval) override
739 + {
740 + return searcher.query_to(retval);
741 + }
742 +
743 + STDMETHOD(CreateUpdateDownloader)(IUpdateDownloader** retval) override
744 + {
745 + return downloader.query_to(retval);
746 + }
747 +
748 + STDMETHOD(CreateUpdateInstaller)(IUpdateInstaller** retval) override
749 + {
750 + return installer.query_to(retval);
751 + }
752 +
753 + STDMETHOD(get_ClientApplicationID)(BSTR*) override
754 + {
755 + return E_NOTIMPL;
756 + }
757 + STDMETHOD(get_ReadOnly)(VARIANT_BOOL*) override
758 + {
759 + return E_NOTIMPL;
760 + }
761 + STDMETHOD(get_WebProxy)(IWebProxy**) override
762 + {
763 + return E_NOTIMPL;
764 + }
765 + STDMETHOD(put_WebProxy)(IWebProxy*) override
766 + {
767 + return E_NOTIMPL;
768 + }
769 +};
770 +
771 +// ---------------------------------------------------------------------------
772 +// MockWindowsUpdateClassFactory
773 +// ---------------------------------------------------------------------------
774 +struct MockWindowsUpdateClassFactory : public WindowsUpdateClassFactory
775 +{
776 + wil::com_ptr<MockUpdateSession> session = wil::MakeOrThrow<MockUpdateSession>();
777 + // Tracks the most-recently created collection (used as toDownload in DownloadUpdates).
778 + mutable wil::com_ptr<MockUpdateCollection> lastCreatedCollection;
779 +
780 + wil::com_ptr<IUpdateSession> CreateUpdateSession() const override
781 + {
782 + return session.query<IUpdateSession>();
783 + }
784 +
785 + wil::com_ptr<IUpdateCollection> CreateUpdateCollection() const override
786 + {
787 + auto col = wil::MakeOrThrow<MockUpdateCollection>();
788 + lastCreatedCollection = col;
789 + wil::com_ptr<IUpdateCollection> result;
790 + col.CopyTo(IID_IUpdateCollection, result.put_void());
791 + return result;
792 + }
793 +};
794 +
795 +// Captures the HRESULT thrown by a wil::ResultException, or S_OK if no exception.
796 +static HRESULT CaptureHResult(const std::function<void()>& fn)
797 +{
798 + try
799 + {
800 + fn();
801 + return S_OK;
802 + }
803 + catch (const wil::ResultException& e)
804 + {
805 + return e.GetErrorCode();
806 + }
807 +}
808 +
809 +// Adds a MockUpdate with the given isDownloaded state to the search result collection.
810 +static wil::com_ptr<MockUpdate> AddMockUpdate(MockUpdateCollection* col, VARIANT_BOOL isDownloaded)
811 +{
812 + auto u = wil::MakeOrThrow<MockUpdate>();
813 + u->isDownloaded = isDownloaded;
814 + wil::com_ptr<IUpdate> update;
815 + u.CopyTo(IID_IUpdate, update.put_void());
816 + col->items.push_back(std::move(update));
817 + return u;
818 +}
819 +
820 +} // namespace
821 +
822 +class WindowsUpdateTests
823 +{
824 + WSL_TEST_CLASS(WindowsUpdateTests)
825 +
826 + // -----------------------------------------------------------------------
827 + // SearchForUpdates tests
828 + // -----------------------------------------------------------------------
829 +
830 + TEST_METHOD(SearchForUpdates_NoUpdates)
831 + {
832 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
833 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
834 +
835 + VERIFY_ARE_EQUAL(0u, ctx.SearchForUpdates());
836 + VERIFY_ARE_EQUAL(0u, ctx.GetUpdateCount());
837 + }
838 +
839 + TEST_METHOD(SearchForUpdates_UpdatesFound)
840 + {
841 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
842 + auto* fp = factory.get();
843 + auto* col = fp->session->searcher->searchResult->updates.get();
844 + AddMockUpdate(col, VARIANT_FALSE);
845 + AddMockUpdate(col, VARIANT_FALSE);
846 + AddMockUpdate(col, VARIANT_FALSE);
847 +
848 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
849 +
850 + VERIFY_ARE_EQUAL(3u, ctx.SearchForUpdates());
851 + VERIFY_ARE_EQUAL(3u, ctx.GetUpdateCount());
852 + }
853 +
854 + TEST_METHOD(SearchForUpdates_SucceededWithErrors)
855 + {
856 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
857 + auto* fp = factory.get();
858 + fp->session->searcher->searchResult->resultCode = OperationResultCode::orcSucceededWithErrors;
859 + AddMockUpdate(fp->session->searcher->searchResult->updates.get(), VARIANT_FALSE);
860 +
861 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
862 +
863 + // orcSucceededWithErrors must succeed — the update count is still returned.
864 + VERIFY_ARE_EQUAL(1u, ctx.SearchForUpdates());
865 + }
866 +
867 + TEST_METHOD(SearchForUpdates_Failed)
868 + {
869 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
870 + factory->session->searcher->searchResult->resultCode = OperationResultCode::orcFailed;
871 +
872 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
873 +
874 + VERIFY_ARE_EQUAL(WSLC_E_WU_SEARCH_FAILED, CaptureHResult([&] { ctx.SearchForUpdates(); }));
875 + }
876 +
877 + // -----------------------------------------------------------------------
878 + // DownloadUpdates tests
879 + // -----------------------------------------------------------------------
880 +
881 + TEST_METHOD(DownloadUpdates_AllAlreadyDownloaded)
882 + {
883 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
884 + auto* fp = factory.get();
885 + auto* col = fp->session->searcher->searchResult->updates.get();
886 + AddMockUpdate(col, VARIANT_TRUE);
887 + AddMockUpdate(col, VARIANT_TRUE);
888 +
889 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
890 + ctx.SearchForUpdates();
891 +
892 + std::vector<uint32_t> progressCalls;
893 + ctx.DownloadUpdates([&](uint32_t p) { progressCalls.push_back(p); });
894 +
895 + // All updates were already downloaded: BeginDownload must not be called,
896 + // and progress(100) must be reported to signal completion.
897 + VERIFY_IS_FALSE(fp->session->downloader->beginDownloadCalled);
898 + VERIFY_ARE_EQUAL(0L, fp->lastCreatedCollection->addCallCount);
899 + VERIFY_ARE_EQUAL(1u, progressCalls.size());
900 + VERIFY_ARE_EQUAL(100u, progressCalls[0]);
901 + }
902 +
903 + TEST_METHOD(DownloadUpdates_SomeNeedDownloading)
904 + {
905 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
906 + auto* fp = factory.get();
907 + auto* col = fp->session->searcher->searchResult->updates.get();
908 + AddMockUpdate(col, VARIANT_TRUE); // already downloaded — skip
909 + AddMockUpdate(col, VARIANT_FALSE); // needs download
910 + AddMockUpdate(col, VARIANT_FALSE); // needs download
911 +
912 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
913 + ctx.SearchForUpdates();
914 + ctx.DownloadUpdates();
915 +
916 + VERIFY_IS_TRUE(fp->session->downloader->beginDownloadCalled);
917 + VERIFY_ARE_EQUAL(2L, fp->lastCreatedCollection->addCallCount);
918 + }
919 +
920 + TEST_METHOD(DownloadUpdates_DownloadFails)
921 + {
922 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
923 + auto* fp = factory.get();
924 + AddMockUpdate(fp->session->searcher->searchResult->updates.get(), VARIANT_FALSE);
925 + fp->session->downloader->downloadResult->downloadHResult = E_FAIL;
926 +
927 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
928 + ctx.SearchForUpdates();
929 +
930 + VERIFY_ARE_EQUAL(E_FAIL, CaptureHResult([&] { ctx.DownloadUpdates(); }));
931 + }
932 +
933 + // -----------------------------------------------------------------------
934 + // InstallUpdates tests
935 + // -----------------------------------------------------------------------
936 +
937 + TEST_METHOD(InstallUpdates_Success)
938 + {
939 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
940 + auto* fp = factory.get();
941 + AddMockUpdate(fp->session->searcher->searchResult->updates.get(), VARIANT_TRUE);
942 +
943 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
944 + ctx.SearchForUpdates();
945 +
946 + // Should not throw.
947 + ctx.InstallUpdates();
948 +
949 + VERIFY_IS_TRUE(fp->session->installer->beginInstallCalled);
950 + VERIFY_IS_NOT_NULL(fp->session->installer->capturedCollection.get());
951 + }
952 +
953 + TEST_METHOD(InstallUpdates_Fails)
954 + {
955 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
956 + auto* fp = factory.get();
957 + AddMockUpdate(fp->session->searcher->searchResult->updates.get(), VARIANT_TRUE);
958 + fp->session->installer->installResult->installHResult = E_ACCESSDENIED;
959 +
960 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
961 + ctx.SearchForUpdates();
962 +
963 + VERIFY_ARE_EQUAL(E_ACCESSDENIED, CaptureHResult([&] { ctx.InstallUpdates(); }));
964 + }
965 +
966 + // -----------------------------------------------------------------------
967 + // RunUpdateFlow tests
968 + // -----------------------------------------------------------------------
969 +
970 + TEST_METHOD(RunUpdateFlow_NoUpdates_ProgressGoesTo100)
971 + {
972 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
973 + auto* fp = factory.get();
974 +
975 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
976 +
977 + std::vector<uint32_t> progressCalls;
978 + ctx.RunUpdateFlow(false, [&](uint32_t p) { progressCalls.push_back(p); });
979 +
980 + // progress(0) at the start, progress(100) because there are no updates.
981 + VERIFY_ARE_EQUAL(2u, progressCalls.size());
982 + VERIFY_ARE_EQUAL(0u, progressCalls[0]);
983 + VERIFY_ARE_EQUAL(100u, progressCalls[1]);
984 +
985 + // No download or install should have been triggered.
986 + VERIFY_IS_FALSE(fp->session->downloader->beginDownloadCalled);
987 + VERIFY_IS_FALSE(fp->session->installer->beginInstallCalled);
988 + }
989 +
990 + TEST_METHOD(RunUpdateFlow_UpdatesFound_DownloadThenInstall)
991 + {
992 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
993 + auto* fp = factory.get();
994 + AddMockUpdate(fp->session->searcher->searchResult->updates.get(), VARIANT_FALSE);
995 +
996 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
997 +
998 + std::vector<uint32_t> progressCalls;
999 + ctx.RunUpdateFlow(false, [&](uint32_t p) { progressCalls.push_back(p); });
1000 +
1001 + // progress(0) is emitted at the start.
1002 + VERIFY_IS_FALSE(progressCalls.empty());
1003 + VERIFY_ARE_EQUAL(0u, progressCalls[0]);
1004 +
1005 + // Both download and install phases ran.
1006 + VERIFY_IS_TRUE(fp->session->downloader->beginDownloadCalled);
1007 + VERIFY_IS_TRUE(fp->session->installer->beginInstallCalled);
1008 + }
1009 +
1010 + TEST_METHOD(RunUpdateFlow_DownloadProgressScaling)
1011 + {
1012 + // Verify that download progress values are scaled into the 0–DownloadProgressPercent range.
1013 + // The download lambda is: progress((percent * DownloadProgressPercent) / 100)
1014 + // For percent=50: expected outer value = (50 * 70) / 100 = 35
1015 + VERIFY_ARE_EQUAL(35u, (50u * WindowsUpdateContext::DownloadProgressPercent) / 100u);
1016 +
1017 + // Verify that install progress values are offset and scaled into the remaining range.
1018 + // The install lambda is: progress(DownloadProgressPercent + (percent * InstallProgressPercent) / 100)
1019 + // For percent=100: expected outer value = 70 + (100 * 30) / 100 = 100
1020 + VERIFY_ARE_EQUAL(100u, WindowsUpdateContext::DownloadProgressPercent + (100u * WindowsUpdateContext::InstallProgressPercent) / 100u);
1021 + }
1022 +
1023 + TEST_METHOD(RunUpdateFlow_DownloadFails_Propagates)
1024 + {
1025 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
1026 + auto* fp = factory.get();
1027 + AddMockUpdate(fp->session->searcher->searchResult->updates.get(), VARIANT_FALSE);
1028 + fp->session->downloader->downloadResult->downloadHResult = E_FAIL;
1029 +
1030 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
1031 +
1032 + VERIFY_ARE_EQUAL(E_FAIL, CaptureHResult([&] { ctx.RunUpdateFlow(); }));
1033 +
1034 + // Install should not have been called after download failure.
1035 + VERIFY_IS_FALSE(fp->session->installer->beginInstallCalled);
1036 + }
1037 +
1038 + TEST_METHOD(RunUpdateFlow_NoProgress_DoesNotCrash)
1039 + {
1040 + // Verifies that passing no progress callback does not crash.
1041 + auto factory = std::make_unique<MockWindowsUpdateClassFactory>();
1042 + AddMockUpdate(factory->session->searcher->searchResult->updates.get(), VARIANT_TRUE);
1043 +
1044 + WindowsUpdateContext ctx(std::move(factory), L"TestProduct");
1045 +
1046 + // Should complete without crashing even with no progress callback.
1047 + ctx.RunUpdateFlow();
1048 + }
1049 +};
test/windows/WslcSdkTests.cpp new
+2397
@@ -0,0 +1,2397 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WslcSdkTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains test cases for the WSLC SDK.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "Common.h"
17 +#include "wslcsdk.h"
18 +#include "WslcsdkPrivate.h"
19 +#include "WSLCContainerLauncher.h"
20 +#include "wslc_schema.h"
21 +#include <optional>
22 +
23 +extern std::wstring g_testDataPath;
24 +extern bool g_fastTestRun;
25 +
26 +using namespace std::chrono_literals;
27 +
28 +namespace {
29 +
30 +//
31 +// RAII guards for opaque WSLC handle types.
32 +//
33 +
34 +void CloseSession(WslcSession session)
35 +{
36 + if (session)
37 + {
38 + WslcTerminateSession(session);
39 + WslcReleaseSession(session);
40 + }
41 +}
42 +
43 +using UniqueSession = wil::unique_any<WslcSession, decltype(CloseSession), CloseSession>;
44 +
45 +void CloseContainer(WslcContainer container)
46 +{
47 + if (container)
48 + {
49 + WslcStopContainer(container, WSLC_SIGNAL_SIGKILL, 0, nullptr);
50 + WslcDeleteContainer(container, WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr);
51 + WslcReleaseContainer(container);
52 + }
53 +}
54 +
55 +using UniqueContainer = wil::unique_any<WslcContainer, decltype(CloseContainer), CloseContainer>;
56 +
57 +void CloseProcess(WslcProcess process)
58 +{
59 + if (process)
60 + {
61 + WslcReleaseProcess(process);
62 + }
63 +}
64 +
65 +using UniqueProcess = wil::unique_any<WslcProcess, decltype(CloseProcess), CloseProcess>;
66 +
67 +struct ProcessOutput
68 +{
69 + std::string stdoutOutput;
70 + std::string stderrOutput;
71 +};
72 +
73 +ProcessOutput WaitForProcessOutput(WslcProcess process, std::chrono::milliseconds timeout = 2min)
74 +{
75 + // Borrow the exit-event handle (lifetime tied to the process object; do NOT close it).
76 + HANDLE exitEvent = nullptr;
77 + THROW_IF_FAILED(WslcGetProcessExitEvent(process, &exitEvent));
78 +
79 + // Acquire stdout / stderr pipe handles (caller owns these).
80 + wil::unique_handle ownedStdout;
81 + THROW_IF_FAILED(WslcGetProcessIOHandle(process, WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));
82 +
83 + wil::unique_handle ownedStderr;
84 + THROW_IF_FAILED(WslcGetProcessIOHandle(process, WSLC_PROCESS_IO_HANDLE_STDERR, &ownedStderr));
85 +
86 + // Read stdout / stderr concurrently so that full pipe buffers do not stall the process.
87 + ProcessOutput output;
88 + wsl::windows::common::relay::MultiHandleWait io;
89 +
90 + io.AddHandle(std::make_unique<wsl::windows::common::relay::ReadHandle>(
91 + std::move(ownedStdout), [&](const auto& buffer) { output.stdoutOutput.append(buffer.data(), buffer.size()); }));
92 +
93 + io.AddHandle(std::make_unique<wsl::windows::common::relay::ReadHandle>(
94 + std::move(ownedStderr), [&](const auto& buffer) { output.stderrOutput.append(buffer.data(), buffer.size()); }));
95 +
96 + auto timeoutTime = std::chrono::steady_clock::now() + timeout;
97 + io.Run(timeout);
98 +
99 + auto remaining = timeoutTime - std::chrono::steady_clock::now();
100 + if (remaining < 0ns)
101 + {
102 + remaining = {};
103 + }
104 +
105 + // Check that the process exits within the timeout.
106 + THROW_HR_IF(
107 + HRESULT_FROM_WIN32(WAIT_TIMEOUT),
108 + WaitForSingleObject(exitEvent, static_cast<DWORD>(std::chrono::duration_cast<std::chrono::milliseconds>(remaining).count())) != WAIT_OBJECT_0);
109 +
110 + return output;
111 +}
112 +
113 +//
114 +// Runs a container with the given argv, waits up to timeoutMs for it to exit,
115 +// and returns the captured stdout / stderr output.
116 +//
117 +ProcessOutput RunContainerAndCapture(WslcSession session, const WslcContainerSettings& containerSettings, std::chrono::milliseconds timeout = 2min)
118 +{
119 + // Create and start the container.
120 + UniqueContainer container;
121 + THROW_IF_FAILED(WslcCreateContainer(session, &containerSettings, &container, nullptr));
122 + THROW_IF_FAILED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
123 +
124 + // Acquire the init process handle.
125 + UniqueProcess process;
126 + THROW_IF_FAILED(WslcGetContainerInitProcess(container.get(), &process));
127 +
128 + return WaitForProcessOutput(process.get());
129 +}
130 +
131 +ProcessOutput RunContainerAndCapture(
132 + WslcSession session,
133 + const char* image,
134 + const std::vector<const char*>& argv,
135 + WslcContainerFlags flags = WSLC_CONTAINER_FLAG_NONE,
136 + const char* name = nullptr,
137 + std::chrono::milliseconds timeout = 2min,
138 + std::optional<WslcContainerNetworkingMode> networkingMode = std::nullopt)
139 +{
140 + // Build process settings.
141 + WslcProcessSettings procSettings;
142 + THROW_IF_FAILED(WslcInitProcessSettings(&procSettings));
143 + if (!argv.empty())
144 + {
145 + THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&procSettings, argv.data(), argv.size()));
146 + }
147 +
148 + // Build container settings.
149 + WslcContainerSettings containerSettings;
150 + THROW_IF_FAILED(WslcInitContainerSettings(image, &containerSettings));
151 + THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
152 + THROW_IF_FAILED(WslcSetContainerSettingsFlags(&containerSettings, flags));
153 + if (name)
154 + {
155 + THROW_IF_FAILED(WslcSetContainerSettingsName(&containerSettings, name));
156 + }
157 + if (networkingMode.has_value())
158 + {
159 + THROW_IF_FAILED(WslcSetContainerSettingsNetworkingMode(&containerSettings, *networkingMode));
160 + }
161 +
162 + return RunContainerAndCapture(session, containerSettings, timeout);
163 +}
164 +
165 +} // namespace
166 +
167 +class WslcSdkTests
168 +{
169 + WSLC_TEST_CLASS(WslcSdkTests)
170 +
171 + WSADATA m_wsadata;
172 + std::filesystem::path m_storagePath;
173 + WslcSession m_defaultSession = nullptr;
174 + static inline auto c_testSessionName = L"wslc-test";
175 +
176 + void LoadTestImage(std::string_view imageName)
177 + {
178 + std::filesystem::path imagePath = GetTestImagePath(imageName);
179 + THROW_IF_FAILED(WslcLoadSessionImageFromFile(m_defaultSession, imagePath.c_str(), nullptr, nullptr));
180 + }
181 +
182 + TEST_CLASS_SETUP(TestClassSetup)
183 + {
184 + THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata));
185 +
186 + // Use the same storage path as WSLC runtime tests to reduce pull overhead.
187 + m_storagePath = std::filesystem::current_path() / "test-storage";
188 +
189 + // Build session settings using the WSLC SDK.
190 + WslcSessionSettings sessionSettings;
191 + VERIFY_SUCCEEDED(WslcInitSessionSettings(c_testSessionName, m_storagePath.c_str(), &sessionSettings));
192 + VERIFY_SUCCEEDED(WslcSetSessionSettingsCpuCount(&sessionSettings, 4));
193 + VERIFY_SUCCEEDED(WslcSetSessionSettingsMemory(&sessionSettings, 2048));
194 + VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
195 +
196 + WslcVhdRequirements vhdReqs{};
197 + vhdReqs.sizeBytes = 4096ull * 1024 * 1024; // 4 GB
198 + vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC;
199 + VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs));
200 +
201 + VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &m_defaultSession, nullptr));
202 +
203 + // Pull images required by the tests (no-op if already present).
204 + for (const char* image : {"debian:latest", "python:3.12-alpine", "hello-world:latest", "wslc-registry:latest"})
205 + {
206 + LoadTestImage(image);
207 + }
208 +
209 + return true;
210 + }
211 +
212 + TEST_CLASS_CLEANUP(TestClassCleanup)
213 + {
214 + if (m_defaultSession)
215 + {
216 + WslcTerminateSession(m_defaultSession);
217 + WslcReleaseSession(m_defaultSession);
218 + m_defaultSession = nullptr;
219 + }
220 +
221 + // Preserve the VHD in fast-run mode so subsequent runs skip image pulling.
222 + if (!g_fastTestRun && !m_storagePath.empty())
223 + {
224 + std::error_code error;
225 + std::filesystem::remove_all(m_storagePath, error);
226 + if (error)
227 + {
228 + LogError("Failed to cleanup storage path %ws: %hs", m_storagePath.c_str(), error.message().c_str());
229 + }
230 + }
231 +
232 + return true;
233 + }
234 +
235 + // -----------------------------------------------------------------------
236 + // Session tests
237 + // -----------------------------------------------------------------------
238 +
239 + WSLC_TEST_METHOD(CreateSession)
240 + {
241 + std::filesystem::path extraStorage = m_storagePath / "wslc-extra-session-storage";
242 +
243 + WslcSessionSettings sessionSettings;
244 + VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-extra-session", extraStorage.c_str(), &sessionSettings));
245 + VERIFY_SUCCEEDED(WslcSetSessionSettingsCpuCount(&sessionSettings, 2));
246 + VERIFY_SUCCEEDED(WslcSetSessionSettingsMemory(&sessionSettings, 1024));
247 + VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
248 +
249 + WslcVhdRequirements vhdReqs{};
250 + vhdReqs.sizeBytes = 1024ull * 1024 * 1024; // 1 GB
251 + vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC;
252 + VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs));
253 +
254 + UniqueSession session;
255 + VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
256 + VERIFY_IS_NOT_NULL(session.get());
257 +
258 + // Null output pointer must fail.
259 + VERIFY_ARE_EQUAL(WslcCreateSession(&sessionSettings, nullptr, nullptr), E_POINTER);
260 +
261 + // Null settings pointer must fail.
262 + UniqueSession session2;
263 + VERIFY_ARE_EQUAL(WslcCreateSession(nullptr, &session2, nullptr), E_POINTER);
264 + }
265 +
266 + WSLC_TEST_METHOD(TerminationCallbackViaTerminate)
267 + {
268 + std::promise<WslcSessionTerminationReason> promise;
269 +
270 + auto callback = [](WslcSessionTerminationReason reason, PVOID context) {
271 + auto* p = static_cast<std::promise<WslcSessionTerminationReason>*>(context);
272 + p->set_value(reason);
273 + };
274 +
275 + std::filesystem::path extraStorage = m_storagePath / "wslc-termcb-term-storage";
276 +
277 + WslcSessionSettings sessionSettings;
278 + VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-termcb-term-test", extraStorage.c_str(), &sessionSettings));
279 + VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
280 + VERIFY_SUCCEEDED(WslcSetSessionSettingsTerminationCallback(&sessionSettings, callback, &promise));
281 +
282 + UniqueSession session;
283 + VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
284 +
285 + // Terminating the session should trigger a graceful shutdown and fire the callback.
286 + VERIFY_SUCCEEDED(WslcTerminateSession(session.get()));
287 +
288 + auto future = promise.get_future();
289 + VERIFY_ARE_EQUAL(future.wait_for(std::chrono::seconds(30)), std::future_status::ready);
290 + VERIFY_ARE_EQUAL(future.get(), WSLC_SESSION_TERMINATION_REASON_SHUTDOWN);
291 + }
292 +
293 + WSLC_TEST_METHOD(TerminationCallbackViaRelease)
294 + {
295 + std::promise<WslcSessionTerminationReason> promise;
296 +
297 + auto callback = [](WslcSessionTerminationReason reason, PVOID context) {
298 + auto* p = static_cast<std::promise<WslcSessionTerminationReason>*>(context);
299 + p->set_value(reason);
300 + };
301 +
302 + std::filesystem::path extraStorage = m_storagePath / "wslc-termcb-release-storage";
303 +
304 + WslcSessionSettings sessionSettings;
305 + VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-termcb-release-test", extraStorage.c_str(), &sessionSettings));
306 + VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
307 + VERIFY_SUCCEEDED(WslcSetSessionSettingsTerminationCallback(&sessionSettings, callback, &promise));
308 +
309 + UniqueSession session;
310 + VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
311 +
312 + // Releasing the session should trigger a graceful shutdown and fire the callback.
313 + VERIFY_SUCCEEDED(WslcReleaseSession(session.get()));
314 + // Calling WslcSessionRelease will destroy the session
315 + session.release();
316 +
317 + auto future = promise.get_future();
318 + VERIFY_ARE_EQUAL(future.wait_for(std::chrono::seconds(30)), std::future_status::ready);
319 + VERIFY_ARE_EQUAL(future.get(), WSLC_SESSION_TERMINATION_REASON_SHUTDOWN);
320 + }
321 +
322 + // -----------------------------------------------------------------------
323 + // Image tests
324 + // -----------------------------------------------------------------------
325 +
326 + WSLC_TEST_METHOD(ImageList)
327 + {
328 + // Positive: session has images pre-loaded — list must return at least one entry.
329 + {
330 + WslcImageInfo* images = nullptr;
331 + uint32_t count = 0;
332 + VERIFY_SUCCEEDED(WslcListSessionImages(m_defaultSession, &images, &count));
333 + auto cleanupImages = wil::scope_exit([images]() { CoTaskMemFree(images); });
334 + VERIFY_IS_TRUE(count >= 1);
335 + VERIFY_IS_NOT_NULL(images);
336 + // At least one image must have a non-empty name.
337 + bool foundNonEmpty = false;
338 + for (uint32_t i = 0; i < count; ++i)
339 + {
340 + if (images[i].name[0] != '\0' && (images[i].sha256[0] != 0 || images[i].sha256[31] != 0) &&
341 + images[i].sizeBytes != 0 && images[i].createdUnixTime != 0)
342 + {
343 + foundNonEmpty = true;
344 + break;
345 + }
346 + }
347 + VERIFY_IS_TRUE(foundNonEmpty);
348 + }
349 +
350 + // Negative: null images pointer must fail.
351 + {
352 + uint32_t count = 0;
353 + VERIFY_ARE_EQUAL(WslcListSessionImages(m_defaultSession, nullptr, &count), E_POINTER);
354 + }
355 +
356 + // Negative: null count pointer must fail.
357 + {
358 + WslcImageInfo* images = nullptr;
359 + VERIFY_ARE_EQUAL(WslcListSessionImages(m_defaultSession, &images, nullptr), E_POINTER);
360 + }
361 + }
362 +
363 + WSLC_TEST_METHOD(LoadImage)
364 + {
365 + // Positive: load a saved image tar and verify the image can be run.
366 + {
367 + // Remove the image first (ignore failure if it wasn't present).
368 + WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", nullptr);
369 +
370 + std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
371 + wil::unique_handle imageTarFileHandle{
372 + CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
373 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
374 +
375 + LARGE_INTEGER fileSize{};
376 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
377 +
378 + VERIFY_SUCCEEDED(WslcLoadSessionImage(
379 + m_defaultSession, imageTarFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, nullptr));
380 +
381 + // Verify the loaded image is usable.
382 + auto output = RunContainerAndCapture(m_defaultSession, "hello-world:latest", {});
383 + VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
384 + }
385 +
386 + // Positive: load a saved image tar and verify the image can be run.
387 + {
388 + // Remove the image first (ignore failure if it wasn't present).
389 + WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", nullptr);
390 +
391 + std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
392 +
393 + VERIFY_SUCCEEDED(WslcLoadSessionImageFromFile(m_defaultSession, imageTar.c_str(), nullptr, nullptr));
394 +
395 + // Verify the loaded image is usable.
396 + auto output = RunContainerAndCapture(m_defaultSession, "hello-world:latest", {});
397 + VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
398 + }
399 +
400 + WslcLoadImageOptions opts{};
401 +
402 + // Negative: null ImageHandle must fail.
403 + VERIFY_ARE_EQUAL(WslcLoadSessionImage(m_defaultSession, nullptr, 1, &opts, nullptr), E_INVALIDARG);
404 +
405 + // Negative: INVALID_HANDLE_VALUE must fail.
406 + VERIFY_ARE_EQUAL(WslcLoadSessionImage(m_defaultSession, INVALID_HANDLE_VALUE, 1, &opts, nullptr), E_INVALIDARG);
407 +
408 + // Negative: zero ContentLength must fail.
409 + VERIFY_ARE_EQUAL(WslcLoadSessionImage(m_defaultSession, GetCurrentThreadEffectiveToken(), 0, &opts, nullptr), E_INVALIDARG);
410 +
411 + // Negative: null path must fail.
412 + VERIFY_ARE_EQUAL(WslcLoadSessionImageFromFile(m_defaultSession, nullptr, &opts, nullptr), E_POINTER);
413 + }
414 +
415 + WSLC_TEST_METHOD(ImportImage)
416 + {
417 + const auto exportedImageTar = std::filesystem::path{g_testDataPath} / L"HelloWorldExported.tar";
418 + constexpr auto c_handleImportedImageName = "my-hello-world-handle:test";
419 + constexpr auto c_pathImportedImageName = "my-hello-world-path:test";
420 +
421 + // Positive: import an exported image tar via handle+length and verify the image can be run.
422 + {
423 + WslcDeleteSessionImage(m_defaultSession, c_handleImportedImageName, nullptr);
424 +
425 + auto cleanup = wil::scope_exit(
426 + [this]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, c_handleImportedImageName, nullptr)); });
427 +
428 + wil::unique_handle imageTarFileHandle{CreateFileW(
429 + exportedImageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
430 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
431 +
432 + LARGE_INTEGER fileSize{};
433 + VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
434 +
435 + VERIFY_SUCCEEDED(WslcImportSessionImage(
436 + m_defaultSession, c_handleImportedImageName, imageTarFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, nullptr));
437 +
438 + auto output = RunContainerAndCapture(m_defaultSession, c_handleImportedImageName, {"/hello"});
439 + VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
440 + }
441 +
442 + // Positive: import an exported image tar via path and verify the image can be run.
443 + {
444 +
445 + WslcDeleteSessionImage(m_defaultSession, c_pathImportedImageName, nullptr);
446 +
447 + auto cleanup = wil::scope_exit(
448 + [this]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, c_pathImportedImageName, nullptr)); });
449 +
450 + VERIFY_SUCCEEDED(WslcImportSessionImageFromFile(m_defaultSession, c_pathImportedImageName, exportedImageTar.c_str(), nullptr, nullptr));
451 +
452 + auto output = RunContainerAndCapture(m_defaultSession, c_pathImportedImageName, {"/hello"});
453 + VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
454 + }
455 +
456 + WslcImportImageOptions opts{};
457 +
458 + // Negative: null image name must fail.
459 + VERIFY_ARE_EQUAL(WslcImportSessionImageFromFile(m_defaultSession, nullptr, exportedImageTar.c_str(), &opts, nullptr), E_POINTER);
460 +
461 + // Negative: missing file input must fail.
462 + VERIFY_ARE_EQUAL(WslcImportSessionImageFromFile(m_defaultSession, "missing-file-input:test", nullptr, &opts, nullptr), E_POINTER);
463 +
464 + // Negative: zero ContentLength must fail.
465 + VERIFY_ARE_EQUAL(WslcImportSessionImage(m_defaultSession, "zero-length:test", GetCurrentThreadEffectiveToken(), 0, &opts, nullptr), E_INVALIDARG);
466 + }
467 +
468 + WSLC_TEST_METHOD(LoadImageNonTar)
469 + {
470 + // The load should fail but it just silently ignores the load currently.
471 + SKIP_TEST_NOT_IMPL();
472 +
473 + // Negative: attempt to load a non-tar file.
474 + {
475 + std::filesystem::path pathToSelf = wil::QueryFullProcessImageNameW<std::wstring>(GetCurrentProcess());
476 + wil::unique_handle selfFileHandle{
477 + CreateFileW(pathToSelf.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
478 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == selfFileHandle.get());
479 +
480 + LARGE_INTEGER fileSize{};
481 + VERIFY_IS_TRUE(GetFileSizeEx(selfFileHandle.get(), &fileSize));
482 +
483 + wil::unique_cotaskmem_string errorMsg;
484 + VERIFY_ARE_EQUAL(
485 + WslcLoadSessionImage(m_defaultSession, selfFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, &errorMsg), E_FAIL);
486 + VERIFY_IS_NOT_NULL(errorMsg.get());
487 + }
488 + }
489 +
490 + WSLC_TEST_METHOD(ImportImageNonTar)
491 + {
492 + // Negative: attempt to load a non-tar file.
493 + {
494 + std::filesystem::path pathToSelf = wil::QueryFullProcessImageNameW<std::wstring>(GetCurrentProcess());
495 + wil::unique_handle selfFileHandle{
496 + CreateFileW(pathToSelf.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
497 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == selfFileHandle.get());
498 +
499 + LARGE_INTEGER fileSize{};
500 + VERIFY_IS_TRUE(GetFileSizeEx(selfFileHandle.get(), &fileSize));
501 +
502 + wil::unique_cotaskmem_string errorMsg;
503 + VERIFY_ARE_EQUAL(
504 + WslcImportSessionImage(
505 + m_defaultSession, "import-self:test", selfFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, &errorMsg),
506 + E_FAIL);
507 + VERIFY_IS_NOT_NULL(errorMsg.get());
508 + LogInfo("Import error: %ws", errorMsg.get());
509 + }
510 + }
511 +
512 + WSLC_TEST_METHOD(ImageDelete)
513 + {
514 + VERIFY_IS_TRUE(HasImage("hello-world:latest"));
515 +
516 + // Positive: delete an existing image.
517 + wil::unique_cotaskmem_string errorMsg;
518 + VERIFY_SUCCEEDED(WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", &errorMsg));
519 +
520 + // Verify the image is no longer present in the list.
521 + VERIFY_IS_FALSE(HasImage("hello-world:latest"));
522 +
523 + // Reload the image for subsequent tests.
524 + LoadTestImage("hello-world:latest");
525 +
526 + // Negative: null name must fail.
527 + VERIFY_ARE_EQUAL(WslcDeleteSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER);
528 + }
529 +
530 + // -----------------------------------------------------------------------
531 + // Container lifecycle tests
532 + // -----------------------------------------------------------------------
533 +
534 + WSLC_TEST_METHOD(CreateContainer)
535 + {
536 + // Simple echo — verify stdout is captured correctly.
537 + {
538 + auto output = RunContainerAndCapture(m_defaultSession, "debian:latest", {"/bin/echo", "OK"});
539 + VERIFY_ARE_EQUAL(output.stdoutOutput, "OK\n");
540 + VERIFY_ARE_EQUAL(output.stderrOutput, "");
541 + }
542 +
543 + // Verify stdout and stderr are routed independently.
544 + {
545 + auto output =
546 + RunContainerAndCapture(m_defaultSession, "debian:latest", {"/bin/sh", "-c", "echo stdout && echo stderr >&2"});
547 + VERIFY_ARE_EQUAL(output.stdoutOutput, "stdout\n");
548 + VERIFY_ARE_EQUAL(output.stderrOutput, "stderr\n");
549 + }
550 +
551 + // Verify that creating a container with a non-existent image fails.
552 + {
553 + WslcContainerSettings containerSettings;
554 + VERIFY_SUCCEEDED(WslcInitContainerSettings("invalid-image:notfound", &containerSettings));
555 +
556 + WslcContainer container = nullptr;
557 + wil::unique_cotaskmem_string errorMsg;
558 + VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &container, &errorMsg), WSLC_E_IMAGE_NOT_FOUND);
559 + VERIFY_IS_NULL(container);
560 + }
561 +
562 + // Verify that a null image name is rejected.
563 + {
564 + WslcContainerSettings containerSettings;
565 + VERIFY_ARE_EQUAL(WslcInitContainerSettings(nullptr, &containerSettings), E_POINTER);
566 + }
567 +
568 + // Verify that a null settings pointer is rejected.
569 + {
570 + VERIFY_ARE_EQUAL(WslcInitContainerSettings("debian:latest", nullptr), E_POINTER);
571 + }
572 +
573 + // Verify that a null container output pointer is rejected.
574 + {
575 + WslcContainerSettings containerSettings;
576 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
577 + VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, nullptr, nullptr), E_POINTER);
578 + }
579 + }
580 +
581 + WSLC_TEST_METHOD(ContainerGetID)
582 + {
583 + UniqueContainer container;
584 + WslcContainerSettings containerSettings;
585 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
586 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
587 +
588 + // Positive: ID is returned and is the expected length of hex characters.
589 + CHAR id[WSLC_CONTAINER_ID_BUFFER_SIZE]{};
590 + VERIFY_SUCCEEDED(WslcGetContainerID(container.get(), id));
591 + VERIFY_ARE_EQUAL(strnlen(id, WSLC_CONTAINER_ID_BUFFER_SIZE), static_cast<size_t>(WSLC_CONTAINER_ID_BUFFER_SIZE - 1));
592 +
593 + // Negative: null ID buffer must fail.
594 + VERIFY_ARE_EQUAL(WslcGetContainerID(container.get(), nullptr), E_POINTER);
595 +
596 + VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr));
597 + }
598 +
599 + WSLC_TEST_METHOD(ContainerGetState)
600 + {
601 + WslcProcessSettings procSettings;
602 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
603 + const char* argv[] = {"/bin/sleep", "99"};
604 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
605 +
606 + WslcContainerSettings containerSettings;
607 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
608 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
609 +
610 + UniqueContainer container;
611 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
612 +
613 + // State after creation: CREATED.
614 + {
615 + WslcContainerState state{};
616 + VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
617 + VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_CREATED);
618 + }
619 +
620 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
621 +
622 + // State while running: RUNNING.
623 + {
624 + WslcContainerState state{};
625 + VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
626 + VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_RUNNING);
627 + }
628 +
629 + VERIFY_SUCCEEDED(WslcStopContainer(container.get(), WSLC_SIGNAL_SIGKILL, 0, nullptr));
630 +
631 + // State after stop: EXITED.
632 + {
633 + WslcContainerState state{};
634 + VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
635 + VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_EXITED);
636 + }
637 +
638 + // Negative: null state pointer must fail.
639 + VERIFY_ARE_EQUAL(WslcGetContainerState(container.get(), nullptr), E_POINTER);
640 +
641 + VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr));
642 + }
643 +
644 + WSLC_TEST_METHOD(ContainerStopAndDelete)
645 + {
646 + // Build a long-running container.
647 + WslcProcessSettings procSettings;
648 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
649 + const char* argv[] = {"/bin/sleep", "999"};
650 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
651 +
652 + WslcContainerSettings containerSettings;
653 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
654 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
655 + VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, "wslc-stop-delete-test"));
656 +
657 + UniqueContainer container;
658 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
659 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
660 +
661 + // Acquire and release the init process handle — we won't read its I/O.
662 + {
663 + UniqueProcess process;
664 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
665 + }
666 +
667 + // Stop the container gracefully (after the timeout).
668 + VERIFY_SUCCEEDED(WslcStopContainer(container.get(), WSLC_SIGNAL_SIGKILL, 0, nullptr));
669 +
670 + // Delete the stopped container.
671 + VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr));
672 + }
673 +
674 + WSLC_TEST_METHOD(ProcessIOHandles)
675 + {
676 + // Verify that stdout and stderr can each be read, and are independent streams.
677 + WslcProcessSettings procSettings;
678 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
679 + const char* argv[] = {"/bin/sh", "-c", "printf 'stdout-line\n' ; printf 'stderr-line\n' >&2"};
680 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
681 +
682 + WslcContainerSettings containerSettings;
683 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
684 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
685 + VERIFY_SUCCEEDED(WslcSetContainerSettingsFlags(&containerSettings, WSLC_CONTAINER_FLAG_NONE));
686 +
687 + UniqueContainer container;
688 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
689 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
690 +
691 + UniqueProcess process;
692 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
693 +
694 + HANDLE exitEvent = nullptr;
695 + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
696 +
697 + HANDLE rawStdout = nullptr;
698 + VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &rawStdout));
699 + wil::unique_handle ownedStdout(rawStdout);
700 +
701 + HANDLE rawStderr = nullptr;
702 + VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDERR, &rawStderr));
703 + wil::unique_handle ownedStderr(rawStderr);
704 +
705 + // Verify that each handle can only be acquired once.
706 + {
707 + HANDLE duplicate = nullptr;
708 + VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &duplicate), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
709 + }
710 +
711 + VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 60 * 1000), WAIT_OBJECT_0);
712 + }
713 +
714 + WSLC_TEST_METHOD(ContainerNetworkingMode)
715 + {
716 + // BRIDGED: container should have an eth0 interface in sysfs.
717 + {
718 + auto output = RunContainerAndCapture(
719 + m_defaultSession,
720 + "debian:latest",
721 + {"/bin/sh", "-c", "[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
722 + WSLC_CONTAINER_FLAG_NONE,
723 + nullptr,
724 + 60s,
725 + WSLC_CONTAINER_NETWORKING_MODE_BRIDGED);
726 + VERIFY_ARE_EQUAL(output.stdoutOutput, "HAS_ETH0\n");
727 + }
728 +
729 + // NONE: container should not have an eth0 interface.
730 + {
731 + auto output = RunContainerAndCapture(
732 + m_defaultSession,
733 + "debian:latest",
734 + {"/bin/sh", "-c", "[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
735 + WSLC_CONTAINER_FLAG_NONE,
736 + nullptr,
737 + 60s,
738 + WSLC_CONTAINER_NETWORKING_MODE_NONE);
739 + VERIFY_ARE_EQUAL(output.stdoutOutput, "NO_ETH0\n");
740 + }
741 +
742 + // Invalid networking mode must fail.
743 + {
744 + WslcContainerSettings containerSettings;
745 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
746 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsNetworkingMode(&containerSettings, static_cast<WslcContainerNetworkingMode>(99)), E_INVALIDARG);
747 + }
748 + }
749 +
750 + WSLC_TEST_METHOD(ContainerPortMapping)
751 + {
752 + // Negative: null mappings with nonzero count must fail.
753 + {
754 + WslcContainerSettings containerSettings;
755 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
756 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsPortMappings(&containerSettings, nullptr, 1), E_INVALIDARG);
757 + }
758 +
759 + // Negative: non-null pointer with zero count must fail.
760 + {
761 + WslcContainerSettings containerSettings;
762 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
763 + WslcContainerPortMapping portMappings[1] = {};
764 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsPortMappings(&containerSettings, portMappings, 0), E_INVALIDARG);
765 + }
766 +
767 + // Positive: null mappings with zero count must succeed (clears the mapping).
768 + {
769 + WslcContainerSettings containerSettings;
770 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
771 + VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings, nullptr, 0));
772 + }
773 +
774 + // Negative: port mappings with NONE networking must fail at container creation.
775 + {
776 + WslcContainerSettings containerSettings1;
777 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings1));
778 + VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings1, WSLC_CONTAINER_NETWORKING_MODE_NONE));
779 +
780 + WslcContainerPortMapping mapping{};
781 + mapping.windowsPort = 12342;
782 + mapping.containerPort = 8000;
783 + mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
784 + VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings1, &mapping, 1));
785 +
786 + WslcContainer rawContainer = nullptr;
787 + VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings1, &rawContainer, nullptr), E_INVALIDARG);
788 + VERIFY_IS_NULL(rawContainer);
789 + }
790 +
791 + // Functional: create a container with BRIDGED networking and a port mapping;
792 + // verify that a TCP connection from the host reaches the container.
793 + {
794 + WslcProcessSettings procSettings;
795 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
796 + const char* argv[] = {"python3", "-m", "http.server", "8000"};
797 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
798 + const char* env[] = {"PYTHONUNBUFFERED=1"};
799 + VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env)));
800 +
801 + WslcContainerSettings containerSettings2;
802 + VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings2));
803 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings2, &procSettings));
804 + VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings2, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));
805 +
806 + WslcContainerPortMapping mapping{};
807 + mapping.windowsPort = 12341;
808 + mapping.containerPort = 8000;
809 + mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
810 + VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings2, &mapping, 1));
811 +
812 + UniqueContainer container;
813 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings2, &container, nullptr));
814 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
815 +
816 + UniqueProcess process;
817 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
818 +
819 + wil::unique_handle ownedStdout;
820 + VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));
821 +
822 + WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s);
823 +
824 + ExpectHttpResponse(L"http://127.0.0.1:12341", 200);
825 + }
826 +
827 + // Functional: port mapping with explicit IPv4 windowsAddress (127.0.0.1).
828 + {
829 + WslcProcessSettings procSettings;
830 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
831 + const char* argv[] = {"python3", "-m", "http.server", "8000"};
832 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
833 + const char* env[] = {"PYTHONUNBUFFERED=1"};
834 + VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env)));
835 +
836 + WslcContainerSettings containerSettings3;
837 + VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings3));
838 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings3, &procSettings));
839 + VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings3, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));
840 +
841 + sockaddr_storage addr4{};
842 + auto* sin4 = reinterpret_cast<sockaddr_in*>(&addr4);
843 + sin4->sin_family = AF_INET;
844 + VERIFY_ARE_EQUAL(inet_pton(AF_INET, "127.0.0.1", &sin4->sin_addr), 1);
845 +
846 + WslcContainerPortMapping mapping{};
847 + mapping.windowsPort = 12343;
848 + mapping.containerPort = 8000;
849 + mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
850 + mapping.windowsAddress = &addr4;
851 + VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings3, &mapping, 1));
852 +
853 + UniqueContainer container;
854 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings3, &container, nullptr));
855 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
856 +
857 + UniqueProcess process;
858 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
859 +
860 + wil::unique_handle ownedStdout;
861 + VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));
862 +
863 + WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s);
864 +
865 + ExpectHttpResponse(L"http://127.0.0.1:12343", 200);
866 + }
867 +
868 + // Functional: port mapping with explicit IPv6 windowsAddress (::1).
869 + {
870 + WslcProcessSettings procSettings;
871 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
872 + const char* argv[] = {"python3", "-m", "http.server", "8000", "--bind", "::"};
873 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
874 + const char* env[] = {"PYTHONUNBUFFERED=1"};
875 + VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env)));
876 +
877 + WslcContainerSettings containerSettings4;
878 + VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings4));
879 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings4, &procSettings));
880 + VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings4, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));
881 +
882 + sockaddr_storage addr6{};
883 + auto* sin6 = reinterpret_cast<sockaddr_in6*>(&addr6);
884 + sin6->sin6_family = AF_INET6;
885 + VERIFY_ARE_EQUAL(inet_pton(AF_INET6, "::1", &sin6->sin6_addr), 1);
886 +
887 + WslcContainerPortMapping mapping{};
888 + mapping.windowsPort = 12344;
889 + mapping.containerPort = 8000;
890 + mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
891 + mapping.windowsAddress = &addr6;
892 + VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings4, &mapping, 1));
893 +
894 + UniqueContainer container;
895 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings4, &container, nullptr));
896 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
897 +
898 + UniqueProcess process;
899 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
900 +
901 + wil::unique_handle ownedStdout;
902 + VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));
903 +
904 + WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s);
905 +
906 + ExpectHttpResponse(L"http://[::1]:12344", 200);
907 + }
908 +
909 + // Negative: unsupported address family must fail when setting container portmapping values.
910 + {
911 + WslcContainerSettings containerSettings5;
912 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings5));
913 + VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings5, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));
914 +
915 + sockaddr_storage badAddr{};
916 + badAddr.ss_family = AF_UNIX; // unsupported for port mapping
917 +
918 + WslcContainerPortMapping mapping{};
919 + mapping.windowsPort = 12345;
920 + mapping.containerPort = 8000;
921 + mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
922 + mapping.windowsAddress = &badAddr;
923 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsPortMappings(&containerSettings5, &mapping, 1), E_INVALIDARG);
924 + }
925 + }
926 +
927 + WSLC_TEST_METHOD(ContainerVolumeUnit)
928 + {
929 + // Negative: null volumes with nonzero count must fail.
930 + {
931 + WslcContainerSettings containerSettings;
932 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
933 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, nullptr, 1), E_INVALIDARG);
934 + }
935 +
936 + // Negative: non-null pointer with zero count must fail.
937 + {
938 + WslcContainerSettings containerSettings;
939 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
940 + WslcContainerVolume containerVolumes[1] = {};
941 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, 0), E_INVALIDARG);
942 + }
943 +
944 + // Negative: null paths must fail.
945 + {
946 + WslcContainerSettings containerSettings;
947 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
948 + WslcContainerVolume containerVolumes[1] = {nullptr, "/mnt/path"};
949 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG);
950 + }
951 +
952 + {
953 + WslcContainerSettings containerSettings;
954 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
955 + auto currentDirectory = std::filesystem::current_path();
956 + WslcContainerVolume containerVolumes[1] = {currentDirectory.c_str(), nullptr};
957 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG);
958 + }
959 +
960 + // Relative paths must fail.
961 + {
962 + WslcContainerSettings containerSettings;
963 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
964 + WslcContainerVolume containerVolumes[1] = {L"relative", "/mnt/path"};
965 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG);
966 + }
967 +
968 + {
969 + WslcContainerSettings containerSettings;
970 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
971 + auto currentDirectory = std::filesystem::current_path();
972 + WslcContainerVolume containerVolumes[1] = {currentDirectory.c_str(), "./mnt/path"};
973 + VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG);
974 + }
975 +
976 + // Positive: null volumes with zero count must succeed (clears volumes).
977 + {
978 + WslcContainerSettings containerSettings;
979 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
980 + VERIFY_SUCCEEDED(WslcSetContainerSettingsVolumes(&containerSettings, nullptr, 0));
981 + }
982 +
983 + // Absolute paths should succeed
984 + {
985 + WslcContainerSettings containerSettings;
986 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
987 + auto currentDirectory = std::filesystem::current_path();
988 + WslcContainerVolume containerVolumes[1] = {currentDirectory.c_str(), "/mnt/path"};
989 + VERIFY_SUCCEEDED(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)));
990 + }
991 + }
992 +
993 + WSLC_TEST_METHOD(ContainerVolumeFunctional)
994 + {
995 + // Functional: mount a read-write and a read-only directory into the container.
996 + {
997 + auto hostRwDir = std::filesystem::current_path() / "wslc-test-vol-rw";
998 + auto hostRoDir = std::filesystem::current_path() / "wslc-test-vol-ro";
999 + std::filesystem::create_directories(hostRwDir);
1000 + std::filesystem::create_directories(hostRoDir);
1001 +
1002 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1003 + std::error_code ec;
1004 + std::filesystem::remove_all(hostRwDir, ec);
1005 + std::filesystem::remove_all(hostRoDir, ec);
1006 + });
1007 +
1008 + // Write sentinel files into both host directories.
1009 + {
1010 + std::ofstream rwSentinel(hostRwDir / "hello.txt");
1011 + rwSentinel << "hello-rw";
1012 + }
1013 + {
1014 + std::ofstream roSentinel(hostRoDir / "hello.txt");
1015 + roSentinel << "hello-ro";
1016 + }
1017 +
1018 + WslcContainerVolume volumes[2]{};
1019 + volumes[0].windowsPath = hostRwDir.c_str();
1020 + volumes[0].containerPath = "/mnt/rw";
1021 + volumes[0].readOnly = FALSE;
1022 + volumes[1].windowsPath = hostRoDir.c_str();
1023 + volumes[1].containerPath = "/mnt/ro";
1024 + volumes[1].readOnly = TRUE;
1025 +
1026 + // Container script:
1027 + // 1. Read from the rw mount.
1028 + // 2. Read from the ro mount.
1029 + // 3. Write a file to the rw mount; print WRITE_OK on success.
1030 + // 4. Try to write to the ro mount; print RO_WRITE_BLOCKED if correctly rejected.
1031 + const char* script =
1032 + "cat /mnt/rw/hello.txt && "
1033 + "cat /mnt/ro/hello.txt && "
1034 + "echo 'container-write' > /mnt/rw/written.txt && echo 'WRITE_OK' && "
1035 + "if touch /mnt/ro/probe 2>/dev/null; then echo 'RO_WRITE_ALLOWED'; else echo 'RO_WRITE_BLOCKED'; fi";
1036 +
1037 + WslcProcessSettings procSettings;
1038 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1039 + const char* argv[] = {"/bin/sh", "-c", script};
1040 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1041 +
1042 + WslcContainerSettings containerSettings;
1043 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1044 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1045 + VERIFY_SUCCEEDED(WslcSetContainerSettingsVolumes(&containerSettings, volumes, 2));
1046 +
1047 + ProcessOutput output = RunContainerAndCapture(m_defaultSession, containerSettings);
1048 +
1049 + // Verify all four outcomes.
1050 + VERIFY_IS_TRUE(output.stdoutOutput.find("hello-rw") != std::string::npos);
1051 + VERIFY_IS_TRUE(output.stdoutOutput.find("hello-ro") != std::string::npos);
1052 + VERIFY_IS_TRUE(output.stdoutOutput.find("WRITE_OK") != std::string::npos);
1053 + VERIFY_IS_TRUE(output.stdoutOutput.find("RO_WRITE_BLOCKED") != std::string::npos);
1054 + VERIFY_IS_TRUE(output.stdoutOutput.find("RO_WRITE_ALLOWED") == std::string::npos);
1055 +
1056 + // Verify the file written by the container is visible on the host.
1057 + std::ifstream written(hostRwDir / "written.txt");
1058 + VERIFY_IS_TRUE(written.is_open());
1059 + std::string writtenContent((std::istreambuf_iterator<char>(written)), std::istreambuf_iterator<char>());
1060 + VERIFY_ARE_EQUAL(writtenContent, "container-write\n");
1061 + }
1062 + }
1063 +
1064 + WSLC_TEST_METHOD(ContainerInspect)
1065 + {
1066 + UniqueContainer container;
1067 + WslcContainerSettings containerSettings;
1068 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1069 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1070 +
1071 + wil::unique_cotaskmem_ansistring inspectData;
1072 + VERIFY_SUCCEEDED(WslcInspectContainer(container.get(), &inspectData));
1073 +
1074 + VERIFY_IS_NOT_NULL(inspectData);
1075 +
1076 + auto inspectObject = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectContainer>(inspectData.get());
1077 +
1078 + CHAR containerId[WSLC_CONTAINER_ID_BUFFER_SIZE];
1079 + VERIFY_SUCCEEDED(WslcGetContainerID(container.get(), containerId));
1080 +
1081 + VERIFY_ARE_EQUAL(containerId, inspectObject.Id);
1082 + }
1083 +
1084 + WSLC_TEST_METHOD(ContainerExec)
1085 + {
1086 + // Start a long-running container so we can exec into it.
1087 + WslcProcessSettings initProcSettings;
1088 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings));
1089 + const char* initArgv[] = {"/bin/sleep", "99"};
1090 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv)));
1091 +
1092 + WslcContainerSettings containerSettings;
1093 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1094 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings));
1095 +
1096 + UniqueContainer container;
1097 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1098 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1099 +
1100 + // Positive: exec an echo command and verify its output.
1101 + {
1102 + WslcProcessSettings execProcSettings;
1103 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings));
1104 + const char* execArgv[] = {"/bin/echo", "exec-hello"};
1105 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv)));
1106 +
1107 + UniqueProcess execProcess;
1108 + VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr));
1109 +
1110 + auto output = WaitForProcessOutput(execProcess.get());
1111 + VERIFY_ARE_EQUAL(output.stdoutOutput, "exec-hello\n");
1112 + }
1113 +
1114 + // Negative: process settings with no command line must fail.
1115 + {
1116 + WslcProcessSettings emptyProcSettings;
1117 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&emptyProcSettings));
1118 + WslcProcess newProcess = nullptr;
1119 + VERIFY_ARE_EQUAL(WslcCreateContainerProcess(container.get(), &emptyProcSettings, &newProcess, nullptr), E_INVALIDARG);
1120 + VERIFY_IS_NULL(newProcess);
1121 + }
1122 +
1123 + // Negative: null newProcess output pointer must fail.
1124 + {
1125 + WslcProcessSettings execProcSettings;
1126 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings));
1127 + const char* execArgv[] = {"/bin/echo", "x"};
1128 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv)));
1129 + VERIFY_ARE_EQUAL(WslcCreateContainerProcess(container.get(), &execProcSettings, nullptr, nullptr), E_POINTER);
1130 + }
1131 + }
1132 +
1133 + WSLC_TEST_METHOD(ContainerHostName)
1134 + {
1135 + // Unit: setting a hostname succeeds.
1136 + {
1137 + WslcContainerSettings containerSettings;
1138 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1139 + VERIFY_SUCCEEDED(WslcSetContainerSettingsHostName(&containerSettings, "test-host"));
1140 + }
1141 +
1142 + // Functional: container process should see the configured hostname.
1143 + {
1144 + WslcProcessSettings procSettings;
1145 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1146 + const char* argv[] = {"/bin/hostname"};
1147 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1148 +
1149 + WslcContainerSettings containerSettings;
1150 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1151 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1152 + VERIFY_SUCCEEDED(WslcSetContainerSettingsHostName(&containerSettings, "my-test-host"));
1153 +
1154 + auto output = RunContainerAndCapture(m_defaultSession, containerSettings);
1155 + VERIFY_ARE_EQUAL(output.stdoutOutput, "my-test-host\n");
1156 + }
1157 + }
1158 +
1159 + WSLC_TEST_METHOD(ContainerDomainName)
1160 + {
1161 + // Unit: setting a domain name succeeds.
1162 + {
1163 + WslcContainerSettings containerSettings;
1164 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1165 + VERIFY_SUCCEEDED(WslcSetContainerSettingsDomainName(&containerSettings, "my.domain"));
1166 + }
1167 +
1168 + // Functional: container should see the configured domain name.
1169 + {
1170 + WslcProcessSettings procSettings;
1171 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1172 + const char* argv[] = {"/bin/sh", "-c", "echo $(domainname)"};
1173 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1174 +
1175 + WslcContainerSettings containerSettings;
1176 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1177 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1178 + VERIFY_SUCCEEDED(WslcSetContainerSettingsDomainName(&containerSettings, "test.local"));
1179 +
1180 + auto output = RunContainerAndCapture(m_defaultSession, containerSettings);
1181 + VERIFY_ARE_EQUAL(output.stdoutOutput, "test.local\n");
1182 + }
1183 + }
1184 +
1185 + WSLC_TEST_METHOD(ProcessEnvVariables)
1186 + {
1187 + // Negative: null pointer with nonzero count must fail.
1188 + {
1189 + WslcProcessSettings procSettings;
1190 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1191 + VERIFY_ARE_EQUAL(WslcSetProcessSettingsEnvVariables(&procSettings, nullptr, 1), E_INVALIDARG);
1192 + }
1193 +
1194 + // Negative: non-null pointer with zero count must fail.
1195 + {
1196 + WslcProcessSettings procSettings;
1197 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1198 + const char* envVars[] = {"FOO=bar"};
1199 + VERIFY_ARE_EQUAL(WslcSetProcessSettingsEnvVariables(&procSettings, envVars, 0), E_INVALIDARG);
1200 + }
1201 +
1202 + // Positive: null pointer with zero count must succeed (clears env vars).
1203 + {
1204 + WslcProcessSettings procSettings;
1205 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1206 + VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, nullptr, 0));
1207 + }
1208 +
1209 + // Functional: set an env var and verify it is visible inside the container.
1210 + {
1211 + WslcProcessSettings procSettings;
1212 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1213 + const char* argv[] = {"/bin/sh", "-c", "echo $MY_TEST_VAR"};
1214 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1215 + const char* envVars[] = {"MY_TEST_VAR=hello-from-test"};
1216 + VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, envVars, ARRAYSIZE(envVars)));
1217 +
1218 + WslcContainerSettings containerSettings;
1219 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1220 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1221 +
1222 + auto output = RunContainerAndCapture(m_defaultSession, containerSettings);
1223 + VERIFY_ARE_EQUAL(output.stdoutOutput, "hello-from-test\n");
1224 + }
1225 + }
1226 +
1227 + WSLC_TEST_METHOD(ProcessSignal)
1228 + {
1229 + WslcProcessSettings procSettings;
1230 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1231 + const char* argv[] = {"/bin/sleep", "99"};
1232 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1233 +
1234 + WslcContainerSettings containerSettings;
1235 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1236 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1237 +
1238 + UniqueContainer container;
1239 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1240 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1241 +
1242 + UniqueProcess process;
1243 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1244 +
1245 + HANDLE exitEvent = nullptr;
1246 + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
1247 +
1248 + // Positive: SIGKILL the running process.
1249 + VERIFY_SUCCEEDED(WslcSignalProcess(process.get(), WSLC_SIGNAL_SIGKILL));
1250 +
1251 + // The process exit event should fire after the signal.
1252 + VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
1253 +
1254 + // Negative: null process handle must return an error.
1255 + VERIFY_ARE_EQUAL(WslcSignalProcess(nullptr, WSLC_SIGNAL_SIGKILL), E_POINTER);
1256 + }
1257 +
1258 + WSLC_TEST_METHOD(ProcessGetPid)
1259 + {
1260 + WslcProcessSettings procSettings;
1261 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1262 + const char* argv[] = {"/bin/sleep", "99"};
1263 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1264 +
1265 + WslcContainerSettings containerSettings;
1266 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1267 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1268 +
1269 + UniqueContainer container;
1270 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1271 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1272 +
1273 + UniqueProcess process;
1274 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1275 +
1276 + // Positive: PID of a running process must be non-zero.
1277 + uint32_t pid = 0;
1278 + VERIFY_SUCCEEDED(WslcGetProcessPid(process.get(), &pid));
1279 + VERIFY_IS_TRUE(pid > 0);
1280 +
1281 + // Negative: null pid pointer must fail.
1282 + VERIFY_ARE_EQUAL(WslcGetProcessPid(process.get(), nullptr), E_POINTER);
1283 +
1284 + // Negative: null process handle must return an error.
1285 + WslcProcess nullProcess = nullptr;
1286 + VERIFY_ARE_EQUAL(WslcGetProcessPid(nullProcess, &pid), E_POINTER);
1287 + }
1288 +
1289 + WSLC_TEST_METHOD(ProcessGetExitCode)
1290 + {
1291 + auto RunAndGetProcess = [&](int exitCodeArg) -> UniqueProcess {
1292 + std::string script = "exit " + std::to_string(exitCodeArg);
1293 + const char* argv[] = {"/bin/sh", "-c", script.c_str()};
1294 +
1295 + WslcProcessSettings procSettings;
1296 + THROW_IF_FAILED(WslcInitProcessSettings(&procSettings));
1297 + THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1298 +
1299 + WslcContainerSettings containerSettings;
1300 + THROW_IF_FAILED(WslcInitContainerSettings("debian:latest", &containerSettings));
1301 + THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1302 +
1303 + UniqueContainer container;
1304 + THROW_IF_FAILED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1305 + THROW_IF_FAILED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
1306 +
1307 + UniqueProcess process;
1308 + THROW_IF_FAILED(WslcGetContainerInitProcess(container.get(), &process));
1309 +
1310 + HANDLE exitEvent = nullptr;
1311 + THROW_IF_FAILED(WslcGetProcessExitEvent(process.get(), &exitEvent));
1312 + THROW_HR_IF(HRESULT_FROM_WIN32(WAIT_TIMEOUT), WaitForSingleObject(exitEvent, 30 * 1000) != WAIT_OBJECT_0);
1313 +
1314 + return process;
1315 + };
1316 +
1317 + auto RunAndGetExitCode = [&](int exitCodeArg) -> INT32 {
1318 + UniqueProcess process = RunAndGetProcess(exitCodeArg);
1319 +
1320 + INT32 code = -1;
1321 + THROW_IF_FAILED(WslcGetProcessExitCode(process.get(), &code));
1322 + return code;
1323 + };
1324 +
1325 + // Positive: verify exit 0 and exit 42 are reported correctly.
1326 + VERIFY_ARE_EQUAL(RunAndGetExitCode(0), 0);
1327 + VERIFY_ARE_EQUAL(RunAndGetExitCode(42), 42);
1328 +
1329 + // Negative: null exit code pointer must fail.
1330 + {
1331 + auto process = RunAndGetProcess(0);
1332 + VERIFY_ARE_EQUAL(WslcGetProcessExitCode(process.get(), nullptr), E_POINTER);
1333 + }
1334 +
1335 + // Negative: null process handle must return an error.
1336 + {
1337 + WslcProcess nullProcess = nullptr;
1338 + INT32 code = 0;
1339 + VERIFY_ARE_EQUAL(WslcGetProcessExitCode(nullProcess, &code), E_POINTER);
1340 + }
1341 + }
1342 +
1343 + WSLC_TEST_METHOD(ProcessGetState)
1344 + {
1345 + WslcProcessSettings procSettings;
1346 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1347 + const char* argv[] = {"/bin/sleep", "99"};
1348 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1349 +
1350 + WslcContainerSettings containerSettings;
1351 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1352 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1353 +
1354 + UniqueContainer container;
1355 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1356 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1357 +
1358 + UniqueProcess process;
1359 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1360 +
1361 + HANDLE exitEvent = nullptr;
1362 + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
1363 +
1364 + // State while running: RUNNING.
1365 + {
1366 + WslcProcessState state{};
1367 + VERIFY_SUCCEEDED(WslcGetProcessState(process.get(), &state));
1368 + VERIFY_ARE_EQUAL(state, WSLC_PROCESS_STATE_RUNNING);
1369 + }
1370 +
1371 + // Bonus test for exit code while running
1372 + {
1373 + INT32 exitCode{};
1374 + VERIFY_ARE_EQUAL(WslcGetProcessExitCode(process.get(), &exitCode), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
1375 + VERIFY_ARE_EQUAL(exitCode, -1);
1376 + }
1377 +
1378 + // Kill the process and wait for the exit event.
1379 + VERIFY_SUCCEEDED(WslcSignalProcess(process.get(), WSLC_SIGNAL_SIGKILL));
1380 + VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
1381 +
1382 + // State after kill: SIGNALLED or EXITED.
1383 + {
1384 + WslcProcessState state{};
1385 + VERIFY_SUCCEEDED(WslcGetProcessState(process.get(), &state));
1386 + VERIFY_IS_TRUE(state == WSLC_PROCESS_STATE_SIGNALLED || state == WSLC_PROCESS_STATE_EXITED);
1387 + }
1388 +
1389 + // Negative: null state pointer must fail.
1390 + VERIFY_ARE_EQUAL(WslcGetProcessState(process.get(), nullptr), E_POINTER);
1391 +
1392 + // Negative: null process handle must return an error.
1393 + {
1394 + WslcProcess nullProcess = nullptr;
1395 + WslcProcessState state{};
1396 + VERIFY_ARE_EQUAL(WslcGetProcessState(nullProcess, &state), E_POINTER);
1397 + }
1398 + }
1399 +
1400 + WSLC_TEST_METHOD(ProcessWorkingDirectory)
1401 + {
1402 + // Unit: setting a working directory returns S_OK.
1403 + {
1404 + WslcProcessSettings procSettings;
1405 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1406 + VERIFY_SUCCEEDED(WslcSetProcessSettingsWorkingDirectory(&procSettings, "/tmp"));
1407 + }
1408 +
1409 + // Negative: null processSettings must fail.
1410 + VERIFY_ARE_EQUAL(WslcSetProcessSettingsWorkingDirectory(nullptr, "/tmp"), E_POINTER);
1411 +
1412 + // Functional: verify pwd reports the configured working directory.
1413 + {
1414 + auto output = RunContainerAndCapture(m_defaultSession, "debian:latest", {"/bin/pwd"});
1415 + // Default working directory baseline — just verify pwd succeeds.
1416 + VERIFY_IS_FALSE(output.stdoutOutput.empty());
1417 + }
1418 +
1419 + // Functional: set working directory to /tmp and verify pwd output.
1420 + {
1421 + WslcProcessSettings procSettings;
1422 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1423 + const char* argv[] = {"/bin/pwd"};
1424 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1425 + VERIFY_SUCCEEDED(WslcSetProcessSettingsWorkingDirectory(&procSettings, "/tmp"));
1426 +
1427 + WslcContainerSettings containerSettings;
1428 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1429 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1430 +
1431 + auto output = RunContainerAndCapture(m_defaultSession, containerSettings);
1432 + VERIFY_ARE_EQUAL(output.stdoutOutput, "/tmp\n");
1433 + }
1434 + }
1435 +
1436 + WSLC_TEST_METHOD(GetVersion)
1437 + {
1438 + // Positive: returns S_OK and fills in a non-zero version.
1439 + {
1440 + WslcVersion version{};
1441 + VERIFY_SUCCEEDED(WslcGetVersion(&version));
1442 + VERIFY_IS_TRUE(version.major > 0 || version.minor > 0 || version.revision > 0);
1443 + }
1444 +
1445 + // Negative: null pointer must fail.
1446 + VERIFY_ARE_EQUAL(WslcGetVersion(nullptr), E_POINTER);
1447 + }
1448 +
1449 + WSLC_TEST_METHOD(GetMissingComponents)
1450 + {
1451 + WslcComponentFlags missing{};
1452 + VERIFY_SUCCEEDED(WslcGetMissingComponents(&missing));
1453 +
1454 + // Presumably anywhere that we run the tests we should get these results.
1455 + // The levels of OS state modification required to test beyond this are beyond the scope of these tests.
1456 + VERIFY_ARE_EQUAL(missing, WSLC_COMPONENT_FLAG_NONE);
1457 + }
1458 +
1459 + // -----------------------------------------------------------------------
1460 + // WslcSetProcessSettingsCallbacks tests
1461 + // -----------------------------------------------------------------------
1462 +
1463 + WSLC_TEST_METHOD(ProcessIoCallbackUnit)
1464 + {
1465 + auto noopIoCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID) {};
1466 + auto noopExitCb = [](INT32, PVOID) {};
1467 +
1468 + // Negative: null processSettings must fail.
1469 + {
1470 + WslcProcessCallbacks callbacks{};
1471 + callbacks.onStdOut = noopIoCb;
1472 + VERIFY_ARE_EQUAL(WslcSetProcessSettingsCallbacks(nullptr, &callbacks, nullptr), E_POINTER);
1473 + }
1474 +
1475 + // Negative: null callbacks pointer with non-null context must fail.
1476 + {
1477 + int ctx = 0;
1478 + WslcProcessSettings procSettings;
1479 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1480 + VERIFY_ARE_EQUAL(WslcSetProcessSettingsCallbacks(&procSettings, nullptr, &ctx), E_INVALIDARG);
1481 + }
1482 +
1483 + // Positive: null callbacks pointer with null context clears all callbacks.
1484 + {
1485 + WslcProcessSettings procSettings;
1486 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1487 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, nullptr, nullptr));
1488 + }
1489 +
1490 + // Positive: set onStdOut only.
1491 + {
1492 + WslcProcessSettings procSettings;
1493 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1494 + WslcProcessCallbacks callbacks{};
1495 + callbacks.onStdOut = noopIoCb;
1496 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1497 + }
1498 +
1499 + // Positive: set onStdErr only.
1500 + {
1501 + WslcProcessSettings procSettings;
1502 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1503 + WslcProcessCallbacks callbacks{};
1504 + callbacks.onStdErr = noopIoCb;
1505 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1506 + }
1507 +
1508 + // Positive: set onExit only.
1509 + {
1510 + WslcProcessSettings procSettings;
1511 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1512 + WslcProcessCallbacks callbacks{};
1513 + callbacks.onExit = noopExitCb;
1514 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1515 + }
1516 +
1517 + // Positive: set all three callbacks with a context.
1518 + {
1519 + int ctx = 0;
1520 + WslcProcessSettings procSettings;
1521 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1522 + WslcProcessCallbacks callbacks{};
1523 + callbacks.onStdOut = noopIoCb;
1524 + callbacks.onStdErr = noopIoCb;
1525 + callbacks.onExit = noopExitCb;
1526 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ctx));
1527 + }
1528 +
1529 + // Negative: StartContainer without ATTACH fails when callbacks are registered.
1530 + // The ATTACH flag is required so the IOCallback can claim the init process pipe handles.
1531 + {
1532 + WslcProcessSettings procSettings;
1533 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1534 + const char* argv[] = {"/bin/sleep", "1"};
1535 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1536 + WslcProcessCallbacks callbacks{};
1537 + callbacks.onStdOut = noopIoCb;
1538 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1539 +
1540 + WslcContainerSettings containerSettings;
1541 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1542 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1543 +
1544 + UniqueContainer container;
1545 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1546 + VERIFY_ARE_EQUAL(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr), E_INVALIDARG);
1547 + }
1548 + }
1549 +
1550 + WSLC_TEST_METHOD(ProcessIoCallbackInitProcess)
1551 + {
1552 + struct IOContext
1553 + {
1554 + std::string stdoutData;
1555 + std::string stderrData;
1556 + } ioContext;
1557 +
1558 + // Both streams share one callback; ioHandle distinguishes which accumulator to use.
1559 + auto ioCb = [](WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t size, PVOID ctx) {
1560 + auto* c = static_cast<IOContext*>(ctx);
1561 + auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? c->stdoutData : c->stderrData;
1562 + target.append(reinterpret_cast<const char*>(data), size);
1563 + };
1564 +
1565 + WslcProcessSettings procSettings;
1566 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1567 + const char* argv[] = {"/bin/sh", "-c", "echo STDOUT && echo STDERR >&2"};
1568 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1569 +
1570 + WslcProcessCallbacks callbacks{};
1571 + callbacks.onStdOut = ioCb;
1572 + callbacks.onStdErr = ioCb;
1573 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ioContext));
1574 +
1575 + WslcContainerSettings containerSettings;
1576 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1577 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1578 +
1579 + UniqueContainer container;
1580 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1581 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
1582 +
1583 + UniqueProcess process;
1584 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1585 +
1586 + HANDLE exitEvent = nullptr;
1587 + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
1588 + VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
1589 +
1590 + // Release the process handle first, then the container handle.
1591 + // Releasing the container destroys the WslcContainerImpl which joins the IOCallback
1592 + // thread, guaranteeing all bytes have been delivered before the assertions below.
1593 + process.reset();
1594 + container.reset();
1595 +
1596 + VERIFY_ARE_EQUAL(ioContext.stdoutData, "STDOUT\n");
1597 + VERIFY_ARE_EQUAL(ioContext.stderrData, "STDERR\n");
1598 + }
1599 +
1600 + WSLC_TEST_METHOD(ProcessIoCallbackExecProcess)
1601 + {
1602 + // Start a long-running container so we can exec into it.
1603 + WslcProcessSettings initProcSettings;
1604 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings));
1605 + const char* initArgv[] = {"/bin/sleep", "99"};
1606 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv)));
1607 +
1608 + WslcContainerSettings containerSettings;
1609 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1610 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings));
1611 +
1612 + UniqueContainer container;
1613 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1614 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1615 +
1616 + struct IOContext
1617 + {
1618 + std::string stdoutData;
1619 + std::string stderrData;
1620 + } ioContext;
1621 +
1622 + auto ioCb = [](WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t size, PVOID ctx) {
1623 + auto* c = static_cast<IOContext*>(ctx);
1624 + auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? c->stdoutData : c->stderrData;
1625 + target.append(reinterpret_cast<const char*>(data), size);
1626 + };
1627 +
1628 + WslcProcessSettings execProcSettings;
1629 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings));
1630 + const char* execArgv[] = {"/bin/sh", "-c", "echo EXEC_OUT && echo EXEC_ERR >&2"};
1631 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv)));
1632 +
1633 + WslcProcessCallbacks callbacks{};
1634 + callbacks.onStdOut = ioCb;
1635 + callbacks.onStdErr = ioCb;
1636 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&execProcSettings, &callbacks, &ioContext));
1637 +
1638 + UniqueProcess execProcess;
1639 + VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr));
1640 +
1641 + HANDLE exitEvent = nullptr;
1642 + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(execProcess.get(), &exitEvent));
1643 + VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
1644 +
1645 + // Releasing the exec process handle destroys WslcProcessImpl and joins its IOCallback
1646 + // thread, ensuring all bytes are delivered before assertions.
1647 + execProcess.reset();
1648 +
1649 + VERIFY_ARE_EQUAL(ioContext.stdoutData, "EXEC_OUT\n");
1650 + VERIFY_ARE_EQUAL(ioContext.stderrData, "EXEC_ERR\n");
1651 + }
1652 +
1653 + WSLC_TEST_METHOD(ProcessIoCallbackHandleExclusion)
1654 + {
1655 + // Register a stdout callback only. IOCallback always acquires ALL pipe handles
1656 + // (draining uncallbacked streams to prevent deadlock), so both stdout and stderr
1657 + // handles are consumed and neither can be obtained via WslcGetProcessIOHandle.
1658 + auto noopIoCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID) {};
1659 +
1660 + WslcProcessSettings procSettings;
1661 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1662 + const char* argv[] = {"/bin/sleep", "99"};
1663 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1664 +
1665 + WslcProcessCallbacks callbacks{};
1666 + callbacks.onStdOut = noopIoCb;
1667 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1668 +
1669 + WslcContainerSettings containerSettings;
1670 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1671 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1672 +
1673 + UniqueContainer container;
1674 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1675 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
1676 +
1677 + UniqueProcess process;
1678 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1679 +
1680 + // stdout handle was consumed by the IOCallback — must not be obtainable.
1681 + {
1682 + HANDLE h = nullptr;
1683 + VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &h), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
1684 + }
1685 +
1686 + // stderr handle was also consumed in order to drain it despite not being given a callback.
1687 + {
1688 + HANDLE h = nullptr;
1689 + VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDERR, &h), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
1690 + }
1691 + }
1692 +
1693 + WSLC_TEST_METHOD(ProcessIoCallbackExitCallback)
1694 + {
1695 + // Verify the onExit callback fires with the correct exit code after IO has been flushed.
1696 + // We test both exit 0 and a non-zero exit code.
1697 + auto RunAndCaptureExit = [&](int exitCodeArg) -> std::pair<INT32, std::string> {
1698 + std::string stdoutData;
1699 + std::atomic<INT32> capturedExitCode{-999};
1700 +
1701 + struct Context
1702 + {
1703 + std::string* stdoutData;
1704 + std::atomic<INT32>* capturedExitCode;
1705 + wil::unique_event exitEvent{wil::EventOptions::ManualReset};
1706 + } ctx{&stdoutData, &capturedExitCode};
1707 +
1708 + auto ioCb = [](WslcProcessIOHandle, const BYTE* data, uint32_t size, PVOID c) {
1709 + static_cast<Context*>(c)->stdoutData->append(reinterpret_cast<const char*>(data), size);
1710 + };
1711 + auto exitCb = [](INT32 code, PVOID c) {
1712 + static_cast<Context*>(c)->capturedExitCode->store(code);
1713 + static_cast<Context*>(c)->exitEvent.SetEvent();
1714 + };
1715 +
1716 + std::string script = "echo HELLO && exit " + std::to_string(exitCodeArg);
1717 + const char* argv[] = {"/bin/sh", "-c", script.c_str()};
1718 +
1719 + WslcProcessSettings procSettings;
1720 + THROW_IF_FAILED(WslcInitProcessSettings(&procSettings));
1721 + THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1722 +
1723 + WslcProcessCallbacks callbacks{};
1724 + callbacks.onStdOut = ioCb;
1725 + callbacks.onExit = exitCb;
1726 + THROW_IF_FAILED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ctx));
1727 +
1728 + WslcContainerSettings containerSettings;
1729 + THROW_IF_FAILED(WslcInitContainerSettings("debian:latest", &containerSettings));
1730 + THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1731 +
1732 + UniqueContainer container;
1733 + THROW_IF_FAILED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1734 + THROW_IF_FAILED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
1735 +
1736 + UniqueProcess process;
1737 + THROW_IF_FAILED(WslcGetContainerInitProcess(container.get(), &process));
1738 +
1739 + THROW_HR_IF(HRESULT_FROM_WIN32(WAIT_TIMEOUT), WaitForSingleObject(ctx.exitEvent.get(), 60 * 1000) != WAIT_OBJECT_0);
1740 +
1741 + return {capturedExitCode.load(), stdoutData};
1742 + };
1743 +
1744 + // Exit 0: onExit must fire with code 0; IO must have been delivered first.
1745 + {
1746 + auto [exitCode, output] = RunAndCaptureExit(0);
1747 + VERIFY_ARE_EQUAL(exitCode, 0);
1748 + VERIFY_ARE_EQUAL(output, "HELLO\n");
1749 + }
1750 +
1751 + // Non-zero exit: onExit must report the correct code.
1752 + {
1753 + auto [exitCode, output] = RunAndCaptureExit(42);
1754 + VERIFY_ARE_EQUAL(exitCode, 42);
1755 + VERIFY_ARE_EQUAL(output, "HELLO\n");
1756 + }
1757 + }
1758 +
1759 + WSLC_TEST_METHOD(ProcessIoCallbackCancelOnRelease)
1760 + {
1761 + // Verify that releasing the process handle while an exec'd process is still running
1762 + // and writing IO cancels the IOCallback pump:
1763 + // - No IO callbacks arrive after the handle is released.
1764 + // - onExit is never invoked (cancellation returns runResult=false, suppressing it).
1765 + //
1766 + // A secondary (exec'd) process is used so that the long-lived init process keeps the
1767 + // container alive, allowing UniqueContainer to clean up normally at scope exit.
1768 +
1769 + // Start a long-running init process to keep the container alive.
1770 + WslcProcessSettings initProcSettings;
1771 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings));
1772 + const char* initArgv[] = {"/bin/sleep", "999"};
1773 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv)));
1774 +
1775 + WslcContainerSettings containerSettings;
1776 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1777 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings));
1778 +
1779 + UniqueContainer container;
1780 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1781 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1782 +
1783 + struct Context
1784 + {
1785 + std::atomic<int> callbackCount{0};
1786 + std::atomic<bool> exitFired{false};
1787 + } ctx;
1788 +
1789 + auto ioCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID c) {
1790 + static_cast<Context*>(c)->callbackCount.fetch_add(1);
1791 + };
1792 + auto exitCb = [](INT32, PVOID c) { static_cast<Context*>(c)->exitFired.store(true); };
1793 +
1794 + // Continuous writer: emits one line every 50 ms indefinitely.
1795 + WslcProcessSettings execProcSettings;
1796 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings));
1797 + const char* execArgv[] = {"/bin/sh", "-c", "while true; do echo LINE; sleep 0.05; done"};
1798 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv)));
1799 +
1800 + WslcProcessCallbacks callbacks{};
1801 + callbacks.onStdOut = ioCb;
1802 + callbacks.onExit = exitCb;
1803 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&execProcSettings, &callbacks, &ctx));
1804 +
1805 + UniqueProcess execProcess;
1806 + VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr));
1807 +
1808 + // Wait long enough for at least several callbacks to arrive.
1809 + Sleep(500);
1810 + VERIFY_IS_TRUE(ctx.callbackCount.load() > 0);
1811 +
1812 + // Release the exec process handle while the process is still running and writing.
1813 + // This destructs WslcProcessImpl → cancels the IOCallback → joins its thread.
1814 + // By the time execProcess.reset() returns, the pump thread has exited.
1815 + execProcess.reset();
1816 +
1817 + // Snapshot the count now that the thread is confirmed stopped.
1818 + int countAtRelease = ctx.callbackCount.load();
1819 +
1820 + // onExit must not have fired: cancellation sets runResult=false, suppressing the call.
1821 + VERIFY_IS_FALSE(ctx.exitFired.load());
1822 +
1823 + // Wait another interval — no further callbacks can arrive after the thread has joined.
1824 + Sleep(200);
1825 + VERIFY_ARE_EQUAL(ctx.callbackCount.load(), countAtRelease);
1826 + VERIFY_IS_FALSE(ctx.exitFired.load());
1827 + }
1828 +
1829 + WSLC_TEST_METHOD(ProcessIoCallbackLargeOutput)
1830 + {
1831 + // Generate ~1 MiB of stdout via: dd if=/dev/zero bs=1024 count=1024 | base64
1832 + // 1,048,576 zero bytes → base64 output is 1,398,104 bytes (ceil(1048576/3)*4).
1833 + static constexpr size_t c_expectedBytes = 1'398'104;
1834 +
1835 + std::string stdoutData;
1836 + stdoutData.reserve(c_expectedBytes + 4096);
1837 +
1838 + auto ioCb = [](WslcProcessIOHandle, const BYTE* data, uint32_t size, PVOID ctx) {
1839 + static_cast<std::string*>(ctx)->append(reinterpret_cast<const char*>(data), size);
1840 + };
1841 +
1842 + WslcProcessSettings procSettings;
1843 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1844 + const char* argv[] = {"/bin/sh", "-c", "dd if=/dev/zero bs=1024 count=1024 2>/dev/null | base64 -w 0"};
1845 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1846 +
1847 + WslcProcessCallbacks callbacks{};
1848 + callbacks.onStdOut = ioCb;
1849 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &stdoutData));
1850 +
1851 + WslcContainerSettings containerSettings;
1852 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1853 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1854 +
1855 + UniqueContainer container;
1856 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1857 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
1858 +
1859 + UniqueProcess process;
1860 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1861 +
1862 + HANDLE exitEvent = nullptr;
1863 + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
1864 + VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 60 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
1865 +
1866 + // Join the IOCallback thread before inspecting the accumulator.
1867 + process.reset();
1868 + container.reset();
1869 +
1870 + VERIFY_ARE_EQUAL(stdoutData.size(), c_expectedBytes);
1871 + }
1872 +
1873 + // -----------------------------------------------------------------------
1874 + // Storage tests
1875 + // -----------------------------------------------------------------------
1876 +
1877 + WSLC_TEST_METHOD(SessionCreateVhd)
1878 + {
1879 + constexpr auto c_volumeName = "wslc-test-data-vol";
1880 + constexpr auto c_vhdSizeBytes = _1GB;
1881 +
1882 + std::filesystem::path vhdSessionStorage = m_storagePath / "wslc-vhd-test-storage";
1883 + auto removeStorage = wil::scope_exit([&]() {
1884 + std::error_code error;
1885 + std::filesystem::remove_all(vhdSessionStorage, error);
1886 + if (error)
1887 + {
1888 + LogError("Failed to remove VHD test storage %ws: %hs", vhdSessionStorage.c_str(), error.message().c_str());
1889 + }
1890 + });
1891 +
1892 + // Create a dedicated session so that volume creation does not affect the shared default session.
1893 + WslcSessionSettings sessionSettings;
1894 + VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-vhd-test", vhdSessionStorage.c_str(), &sessionSettings));
1895 +
1896 + WslcVhdRequirements sessionVhd{};
1897 + sessionVhd.sizeBytes = 4 * _1GB;
1898 + sessionVhd.type = WSLC_VHD_TYPE_DYNAMIC;
1899 + VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &sessionVhd));
1900 +
1901 + UniqueSession session;
1902 + VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
1903 +
1904 + // Load debian so we have a container image to work with.
1905 + std::filesystem::path debianTar = GetTestImagePath("debian:latest");
1906 + VERIFY_SUCCEEDED(WslcLoadSessionImageFromFile(session.get(), debianTar.c_str(), nullptr, nullptr));
1907 +
1908 + // Positive: create a named VHD volume in the session.
1909 + {
1910 + WslcVhdRequirements vhd{};
1911 + vhd.name = c_volumeName;
1912 + vhd.sizeBytes = c_vhdSizeBytes;
1913 + vhd.type = WSLC_VHD_TYPE_DYNAMIC;
1914 + wil::unique_cotaskmem_string errorMsg;
1915 + VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg));
1916 +
1917 + // The backing VHD file must exist on disk.
1918 + std::filesystem::path expectedVhdPath = vhdSessionStorage / "volumes" / (std::string(c_volumeName) + ".vhdx");
1919 + VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath));
1920 + }
1921 +
1922 + // Positive: write a marker via a container that mounts the named volume.
1923 + {
1924 + WslcProcessSettings procSettings;
1925 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1926 + const char* argv[] = {"/bin/sh", "-c", "echo wslc-vhd-test > /data/marker.txt"};
1927 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1928 +
1929 + WslcContainerSettings containerSettings;
1930 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1931 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1932 +
1933 + WslcContainerNamedVolume namedVol{};
1934 + namedVol.name = c_volumeName;
1935 + namedVol.containerPath = "/data";
1936 + namedVol.readOnly = FALSE;
1937 + VERIFY_SUCCEEDED(WslcSetContainerSettingsNamedVolumes(&containerSettings, &namedVol, 1));
1938 +
1939 + auto output = RunContainerAndCapture(session.get(), containerSettings);
1940 + VERIFY_IS_TRUE(output.stderrOutput.empty());
1941 + }
1942 +
1943 + // Positive: read back the marker in a second container (read-only mount).
1944 + {
1945 + WslcProcessSettings procSettings;
1946 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1947 + const char* argv[] = {"/bin/sh", "-c", "cat /data/marker.txt"};
1948 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1949 +
1950 + WslcContainerSettings containerSettings;
1951 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1952 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1953 +
1954 + WslcContainerNamedVolume namedVol{};
1955 + namedVol.name = c_volumeName;
1956 + namedVol.containerPath = "/data";
1957 + namedVol.readOnly = TRUE;
1958 + VERIFY_SUCCEEDED(WslcSetContainerSettingsNamedVolumes(&containerSettings, &namedVol, 1));
1959 +
1960 + auto output = RunContainerAndCapture(session.get(), containerSettings);
1961 + VERIFY_ARE_EQUAL(output.stdoutOutput, "wslc-vhd-test\n");
1962 + }
1963 +
1964 + // Positive: delete the volume.
1965 + {
1966 + wil::unique_cotaskmem_string errorMsg;
1967 + VERIFY_SUCCEEDED(WslcDeleteSessionVhdVolume(session.get(), c_volumeName, &errorMsg));
1968 +
1969 + // The backing VHD file must not exist on disk.
1970 + std::filesystem::path expectedVhdPath = vhdSessionStorage / "volumes" / (std::string(c_volumeName) + ".vhdx");
1971 + VERIFY_IS_FALSE(std::filesystem::exists(expectedVhdPath));
1972 + }
1973 +
1974 + // Negative: null options pointer must fail.
1975 + VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), nullptr, nullptr), E_POINTER);
1976 +
1977 + // Negative: null name must fail.
1978 + {
1979 + WslcVhdRequirements vhd{};
1980 + vhd.name = nullptr;
1981 + vhd.sizeBytes = c_vhdSizeBytes;
1982 + vhd.type = WSLC_VHD_TYPE_DYNAMIC;
1983 + VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG);
1984 + }
1985 +
1986 + // Negative: zero sizeInBytes must fail.
1987 + {
1988 + WslcVhdRequirements vhd{};
1989 + vhd.name = c_volumeName;
1990 + vhd.sizeBytes = 0;
1991 + vhd.type = WSLC_VHD_TYPE_DYNAMIC;
1992 + VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG);
1993 + }
1994 +
1995 + // Negative: fixed VHD type is not yet supported.
1996 + {
1997 + WslcVhdRequirements vhd{};
1998 + vhd.name = c_volumeName;
1999 + vhd.sizeBytes = c_vhdSizeBytes;
2000 + vhd.type = WSLC_VHD_TYPE_FIXED;
2001 + VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_NOTIMPL);
2002 + }
2003 + }
2004 +
2005 + // -----------------------------------------------------------------------
2006 + // Authentication helpers
2007 + // -----------------------------------------------------------------------
2008 +
2009 + // Starts a local registry container with host-mode networking and returns [container, registryAddress].
2010 + // Uses the COM API (via GetInternalType) with WSLCContainerLauncher to get host-mode networking,
2011 + // which the SDK doesn't expose. Host networking shares the VM's network namespace, so the registry
2012 + // is reachable at 127.0.0.1:<port> from both dockerd (inside the VM) and the host.
2013 + std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalRegistry(
2014 + const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000)
2015 + {
2016 + VERIFY_IS_TRUE(HasImage("wslc-registry:latest"));
2017 +
2018 + std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
2019 + if (!username.empty())
2020 + {
2021 + env.push_back(std::format("USERNAME={}", username));
2022 + env.push_back(std::format("PASSWORD={}", password));
2023 + }
2024 +
2025 + wsl::windows::common::WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env);
2026 + launcher.SetEntrypoint({"/entrypoint.sh"});
2027 + launcher.AddPort(port, port, AF_INET);
2028 +
2029 + // Get the IWSLCSession COM object from the SDK session handle.
2030 + auto& session = *reinterpret_cast<WslcSessionImpl*>(m_defaultSession)->session;
2031 + auto container = launcher.Launch(session, WSLCContainerStartFlagsNone);
2032 +
2033 + auto registryAddress = std::format("127.0.0.1:{}", port);
2034 +
2035 + // Wait for the registry to be ready by probing from the host.
2036 + auto hostUrl = std::format(L"http://{}", registryAddress);
2037 + ExpectHttpResponse(hostUrl.c_str(), 200, true);
2038 +
2039 + return {std::move(container), registryAddress};
2040 + }
2041 +
2042 + // Tags and pushes an image to a local registry via the SDK APIs.
2043 + void PushImageToRegistry(const std::string& repo, const std::string& tag, const std::string& registryAddress, const std::string& registryAuth)
2044 + {
2045 + auto imageName = std::format("{}:{}", repo, tag);
2046 + auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag);
2047 + auto registryRepo = std::format("{}/{}", registryAddress, repo);
2048 +
2049 + VERIFY_IS_TRUE(HasImage(imageName));
2050 +
2051 + // Tag the image with the registry address so it can be pushed.
2052 + WslcTagImageOptions tagOptions{};
2053 + tagOptions.image = imageName.c_str();
2054 + tagOptions.repo = registryRepo.c_str();
2055 + tagOptions.tag = tag.c_str();
2056 + VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &tagOptions, nullptr));
2057 +
2058 + // Ensures the registry-prefixed tag is removed after the push.
2059 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2060 + LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, registryImage.c_str(), nullptr));
2061 + });
2062 +
2063 + WslcPushImageOptions pushOptions{};
2064 + pushOptions.image = registryImage.c_str();
2065 + pushOptions.registryAuth = registryAuth.c_str();
2066 + VERIFY_SUCCEEDED(WslcPushSessionImage(m_defaultSession, &pushOptions, nullptr));
2067 + }
2068 +
2069 + bool HasImage(const std::string& imageName)
2070 + {
2071 + wil::unique_cotaskmem_array_ptr<WslcImageInfo> images;
2072 + VERIFY_SUCCEEDED(WslcListSessionImages(m_defaultSession, images.addressof(), images.size_address<uint32_t>()));
2073 +
2074 + for (const auto& image : images)
2075 + {
2076 + if (image.name == imageName)
2077 + {
2078 + return true;
2079 + }
2080 + }
2081 + return false;
2082 + }
2083 +
2084 + // -----------------------------------------------------------------------
2085 + // Authentication tests
2086 + // -----------------------------------------------------------------------
2087 +
2088 + WSLC_TEST_METHOD(AuthenticateTests)
2089 + {
2090 + constexpr auto c_username = "wslctest";
2091 + constexpr auto c_password = "password";
2092 +
2093 + auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password);
2094 +
2095 + // Negative: wrong password must fail.
2096 + {
2097 + wil::unique_cotaskmem_ansistring token;
2098 + wil::unique_cotaskmem_string errorMsg;
2099 + VERIFY_ARE_EQUAL(
2100 + WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, "wrong-password", &token, &errorMsg), E_FAIL);
2101 + VERIFY_IS_NOT_NULL(errorMsg.get());
2102 + }
2103 +
2104 + // Positive: correct credentials must succeed and return a non-null token.
2105 + {
2106 + wil::unique_cotaskmem_ansistring token;
2107 + wil::unique_cotaskmem_string errorMsg;
2108 + VERIFY_SUCCEEDED(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, &token, &errorMsg));
2109 + VERIFY_IS_NOT_NULL(token.get());
2110 + }
2111 +
2112 + auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password);
2113 + PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
2114 +
2115 + auto image = std::format("{}/hello-world:latest", registryAddress);
2116 +
2117 + // Pulling with credentials should succeed.
2118 + {
2119 + WslcPullImageOptions opts{};
2120 + opts.uri = image.c_str();
2121 + opts.registryAuth = xRegistryAuth.c_str();
2122 + VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr));
2123 + VERIFY_IS_TRUE(HasImage(image));
2124 + }
2125 +
2126 + // Negative: Pulling without credentials should fail.
2127 + {
2128 + WslcPullImageOptions opts{};
2129 + opts.uri = image.c_str();
2130 +
2131 + wil::unique_cotaskmem_string errorMsg;
2132 + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL);
2133 + VERIFY_IS_NOT_NULL(errorMsg.get());
2134 + }
2135 +
2136 + // Negative: Pulling with bad credentials should fail.
2137 + {
2138 + auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong");
2139 +
2140 + WslcPullImageOptions opts{};
2141 + opts.uri = image.c_str();
2142 + opts.registryAuth = badAuth.c_str();
2143 +
2144 + wil::unique_cotaskmem_string errorMsg;
2145 + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL);
2146 + VERIFY_IS_NOT_NULL(errorMsg.get());
2147 + }
2148 +
2149 + // Negative: null parameters must fail.
2150 + {
2151 + wil::unique_cotaskmem_ansistring token;
2152 + VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, nullptr, c_username, c_password, &token, nullptr), E_POINTER);
2153 + VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), nullptr, c_password, &token, nullptr), E_POINTER);
2154 + VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, nullptr, &token, nullptr), E_POINTER);
2155 + VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, nullptr, nullptr), E_POINTER);
2156 + }
2157 + }
2158 +
2159 + WSLC_TEST_METHOD(PullImage)
2160 + {
2161 + // Start a local registry without auth to avoid Docker Hub rate limits.
2162 + auto [registryContainer, registryAddress] = StartLocalRegistry();
2163 + auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
2164 +
2165 + {
2166 + // Push hello-world:latest to the local registry.
2167 + PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
2168 +
2169 + auto image = std::format("{}/hello-world:latest", registryAddress);
2170 +
2171 + // Delete the image locally so the pull is a real pull.
2172 + WslcDeleteSessionImage(m_defaultSession, image.c_str(), nullptr);
2173 +
2174 + // Pull from the local registry.
2175 + {
2176 + WslcPullImageOptions opts{};
2177 + opts.uri = image.c_str();
2178 + VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr));
2179 + }
2180 +
2181 + // Verify the pulled image is in the image list.
2182 + VERIFY_IS_TRUE(HasImage(image));
2183 +
2184 + // Verify the image is usable by running a container from it.
2185 + auto output = RunContainerAndCapture(m_defaultSession, image.c_str(), {});
2186 + VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
2187 + }
2188 +
2189 + // Negative: pull an image that does not exist.
2190 + {
2191 + auto image = std::format("{}/does-not-exist", registryAddress);
2192 +
2193 + WslcPullImageOptions opts{};
2194 + opts.uri = image.c_str();
2195 + opts.registryAuth = xRegistryAuth.c_str();
2196 +
2197 + wil::unique_cotaskmem_string errorMsg;
2198 + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), WSLC_E_IMAGE_NOT_FOUND);
2199 + }
2200 +
2201 + // Negative: null URI inside options must fail.
2202 + {
2203 + WslcPullImageOptions opts{};
2204 + opts.uri = nullptr;
2205 +
2206 + wil::unique_cotaskmem_string errorMsg;
2207 + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_INVALIDARG);
2208 + }
2209 + }
2210 +
2211 + WSLC_TEST_METHOD(PushImage)
2212 + {
2213 + auto emptyRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
2214 +
2215 + // Negative: pushing a non-existent image must fail.
2216 + {
2217 + WslcPushImageOptions opts{};
2218 + opts.image = "does-not-exist";
2219 + opts.registryAuth = emptyRegistryAuth.c_str();
2220 +
2221 + wil::unique_cotaskmem_string errorMsg;
2222 + VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL);
2223 + VERIFY_IS_NOT_NULL(errorMsg.get());
2224 + }
2225 +
2226 + // Negative: null options must fail.
2227 + VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER);
2228 +
2229 + // Negative: null image inside options must fail.
2230 + {
2231 + WslcPushImageOptions opts{};
2232 + opts.image = nullptr;
2233 + opts.registryAuth = emptyRegistryAuth.c_str();
2234 +
2235 + VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG);
2236 + }
2237 + }
2238 +
2239 + WSLC_TEST_METHOD(TagImage)
2240 + {
2241 + // Positive: tag an existing image.
2242 + {
2243 + WslcTagImageOptions opts{};
2244 + opts.image = "debian:latest";
2245 + opts.repo = "debian";
2246 + opts.tag = "sdk-test-tag";
2247 + VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &opts, nullptr));
2248 +
2249 + // Verify the tag is present.
2250 + VERIFY_IS_TRUE(HasImage("debian:sdk-test-tag"));
2251 +
2252 + // Cleanup: delete the tag.
2253 + WslcDeleteSessionImage(m_defaultSession, "debian:sdk-test-tag", nullptr);
2254 + }
2255 +
2256 + // Negative: null options must fail.
2257 + VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER);
2258 +
2259 + // Negative: null fields must fail.
2260 + {
2261 + WslcTagImageOptions opts{};
2262 + opts.image = nullptr;
2263 + opts.repo = "debian";
2264 + opts.tag = "test";
2265 + VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG);
2266 + }
2267 + {
2268 + WslcTagImageOptions opts{};
2269 + opts.image = "debian:latest";
2270 + opts.repo = nullptr;
2271 + opts.tag = "test";
2272 + VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG);
2273 + }
2274 + {
2275 + WslcTagImageOptions opts{};
2276 + opts.image = "debian:latest";
2277 + opts.repo = "debian";
2278 + opts.tag = nullptr;
2279 + VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG);
2280 + }
2281 + }
2282 +
2283 + // Negative tests: handle lifecycle and invalid state transitions
2284 +
2285 + WSLC_TEST_METHOD(ReleaseNullSessionHandle)
2286 + {
2287 + VERIFY_ARE_EQUAL(WslcReleaseSession(nullptr), E_POINTER);
2288 + }
2289 +
2290 + WSLC_TEST_METHOD(TerminateNullSessionHandle)
2291 + {
2292 + VERIFY_ARE_EQUAL(WslcTerminateSession(nullptr), E_POINTER);
2293 + }
2294 +
2295 + WSLC_TEST_METHOD(ReleaseNullContainerHandle)
2296 + {
2297 + VERIFY_ARE_EQUAL(WslcReleaseContainer(nullptr), E_POINTER);
2298 + }
2299 +
2300 + WSLC_TEST_METHOD(ReleaseNullProcessHandle)
2301 + {
2302 + VERIFY_ARE_EQUAL(WslcReleaseProcess(nullptr), E_POINTER);
2303 + }
2304 +
2305 + WSLC_TEST_METHOD(CreateContainerWithNullSession)
2306 + {
2307 + WslcContainerSettings containerSettings;
2308 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2309 +
2310 + WslcContainer container = nullptr;
2311 + VERIFY_ARE_EQUAL(WslcCreateContainer(nullptr, &containerSettings, &container, nullptr), E_POINTER);
2312 + }
2313 +
2314 + WSLC_TEST_METHOD(StopContainerWithInvalidSignal)
2315 + {
2316 + UniqueContainer container;
2317 + WslcContainerSettings containerSettings;
2318 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2319 +
2320 + WslcProcessSettings procSettings;
2321 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2322 + PCSTR argv[] = {"/bin/sleep", "10"};
2323 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2324 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2325 +
2326 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
2327 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
2328 +
2329 + // Wait for the short-lived init process to exit
2330 + UniqueProcess initProcess;
2331 + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &initProcess));
2332 + HANDLE exitEvent = nullptr;
2333 + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(initProcess.get(), &exitEvent));
2334 + VERIFY_ARE_EQUAL(WAIT_OBJECT_0, WaitForSingleObject(exitEvent, 30000));
2335 +
2336 + // Attempting to exec on a stopped container should fail
2337 + WslcProcessSettings execSettings;
2338 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&execSettings));
2339 + PCSTR execArgv[] = {"/bin/echo", "should-fail"};
2340 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execSettings, execArgv, ARRAYSIZE(execArgv)));
2341 +
2342 + UniqueProcess execProcess;
2343 + VERIFY_ARE_EQUAL(WslcCreateContainerProcess(container.get(), &execSettings, &execProcess, nullptr), WSLC_E_CONTAINER_NOT_RUNNING);
2344 + }
2345 +
2346 + WSLC_TEST_METHOD(DuplicateContainerName)
2347 + {
2348 + WslcContainerSettings containerSettings;
2349 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2350 +
2351 + WslcProcessSettings procSettings;
2352 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2353 + PCSTR argv[] = {"/bin/sleep", "10"};
2354 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2355 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2356 + VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, "duplicate-name-test"));
2357 +
2358 + UniqueContainer container1;
2359 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container1, nullptr));
2360 + VERIFY_SUCCEEDED(WslcStartContainer(container1.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
2361 +
2362 + // Creating a second container with the same name should fail
2363 + UniqueContainer container2;
2364 + VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &container2, nullptr), static_cast<HRESULT>(0x800700b7)); // ERROR_ALREADY_EXISTS
2365 + }
2366 +
2367 + WSLC_TEST_METHOD(DeleteRunningContainerWithoutForce)
2368 + {
2369 + UniqueContainer container;
2370 + WslcContainerSettings containerSettings;
2371 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2372 +
2373 + WslcProcessSettings procSettings;
2374 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2375 + PCSTR argv[] = {"/bin/sleep", "10"};
2376 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2377 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2378 +
2379 + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
2380 + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
2381 +
2382 + // Deleting a running container without force flag should fail
2383 + VERIFY_ARE_EQUAL(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr), WSLC_E_CONTAINER_IS_RUNNING);
2384 + }
2385 +
2386 + WSLC_TEST_METHOD(DeleteNonExistentImage)
2387 + {
2388 + VERIFY_ARE_EQUAL(WslcDeleteSessionImage(m_defaultSession, "nonexistent-image:this-tag-does-not-exist", nullptr), WSLC_E_IMAGE_NOT_FOUND);
2389 + }
2390 +
2391 + WSLC_TEST_METHOD(PullInvalidImageUri)
2392 + {
2393 + WslcPullImageOptions pullOptions = {};
2394 + pullOptions.uri = "///invalid-registry-url///";
2395 + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &pullOptions, nullptr), E_INVALIDARG);
2396 + }
2397 +};
test/windows/wslc/CMakeLists.txt new
+31
@@ -0,0 +1,31 @@
1 +# WSLC CLI Unit Tests
2 +
3 +file(GLOB_RECURSE WSLC_TEST_SOURCES CONFIGURE_DEPENDS
4 + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
5 +)
6 +
7 +file(GLOB_RECURSE WSLC_TEST_HEADERS CONFIGURE_DEPENDS
8 + ${CMAKE_CURRENT_SOURCE_DIR}/*.h
9 +)
10 +
11 +# Add the sources to the parent wsltests target.
12 +# This ensures they're compiled into the main test binary.
13 +target_sources(wsltests PRIVATE ${WSLC_TEST_SOURCES} ${WSLC_TEST_HEADERS})
14 +
15 +# Ensure the file uses the precompiled header from the parent target.
16 +set_source_files_properties(${WSLC_TEST_SOURCES}
17 + PROPERTIES
18 + COMPILE_FLAGS "/Yuprecomp.h"
19 +)
20 +
21 +# Add include directories needed for WSLC tests.
22 +target_include_directories(wsltests PRIVATE
23 + ${CMAKE_SOURCE_DIR}/test
24 + ${CMAKE_SOURCE_DIR}/test/windows
25 + ${CMAKE_SOURCE_DIR}/test/windows/wslc
26 + ${CMAKE_SOURCE_DIR}/src/windows/wslc/core
27 + ${CMAKE_SOURCE_DIR}/src/windows/wslc/commands
28 + ${CMAKE_SOURCE_DIR}/src/windows/wslc/arguments
29 + ${CMAKE_SOURCE_DIR}/src/windows/wslc/services
30 + ${CMAKE_SOURCE_DIR}/src/windows/wslc/tasks
31 +)
test/windows/wslc/CommandLineTestCases.h new
+189
@@ -0,0 +1,189 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + CommandLineTestCases.h
8 +
9 +Abstract:
10 +
11 + Test case data for command-line parsing tests.
12 +
13 +--*/
14 +
15 +// These cases should be for testing valid command lines against the defined commands.
16 +// This executes the command line parsing logic and verifies that the command line is valid
17 +// for the defined commands. It does not actually execute the command.
18 +
19 +// X-Macro definition: COMMAND_LINE_TEST_CASE(commandLine, expectedCommand, shouldSucceed)
20 +
21 +// Root command tests
22 +COMMAND_LINE_TEST_CASE(L"", L"root", true)
23 +COMMAND_LINE_TEST_CASE(L"--help", L"root", true)
24 +COMMAND_LINE_TEST_CASE(L"-?", L"root", true)
25 +COMMAND_LINE_TEST_CASE(L"--version", L"root", true)
26 +COMMAND_LINE_TEST_CASE(L"-v", L"root", true)
27 +
28 +// Session command tests
29 +COMMAND_LINE_TEST_CASE(L"session list", L"list", true)
30 +COMMAND_LINE_TEST_CASE(L"session list --verbose", L"list", true)
31 +COMMAND_LINE_TEST_CASE(L"session list --verbose --help", L"list", true)
32 +COMMAND_LINE_TEST_CASE(L"session list --notanarg", L"list", false)
33 +COMMAND_LINE_TEST_CASE(L"session list extraarg", L"list", false)
34 +COMMAND_LINE_TEST_CASE(L"session shell session1", L"shell", true)
35 +COMMAND_LINE_TEST_CASE(L"session shell", L"shell", true)
36 +COMMAND_LINE_TEST_CASE(L"session terminate session1", L"terminate", true)
37 +COMMAND_LINE_TEST_CASE(L"session terminate", L"terminate", true)
38 +COMMAND_LINE_TEST_CASE(L"session enter C:\\storage", L"enter", true)
39 +COMMAND_LINE_TEST_CASE(L"session enter C:\\storage --name my-session", L"enter", true)
40 +COMMAND_LINE_TEST_CASE(L"session enter --name my-session C:\\storage", L"enter", true)
41 +COMMAND_LINE_TEST_CASE(L"session enter", L"enter", false) // Missing required storage-path
42 +COMMAND_LINE_TEST_CASE(L"session enter C:\\storage --notanarg", L"enter", false) // Invalid argument
43 +COMMAND_LINE_TEST_CASE(L"session enter --name my-session", L"enter", false) // Missing required positional before flag
44 +
45 +// Container command tests
46 +COMMAND_LINE_TEST_CASE(L"container list", L"list", true)
47 +COMMAND_LINE_TEST_CASE(L"container ls", L"list", true)
48 +COMMAND_LINE_TEST_CASE(L"container ps", L"list", true)
49 +COMMAND_LINE_TEST_CASE(L"list", L"list", true)
50 +COMMAND_LINE_TEST_CASE(L"ls", L"list", true)
51 +COMMAND_LINE_TEST_CASE(L"ps", L"list", true)
52 +COMMAND_LINE_TEST_CASE(L"container list --no-trunc", L"list", true)
53 +COMMAND_LINE_TEST_CASE(L"container list --session foo", L"list", true)
54 +COMMAND_LINE_TEST_CASE(L"container list -qa", L"list", true)
55 +COMMAND_LINE_TEST_CASE(L"container list --format json", L"list", true)
56 +COMMAND_LINE_TEST_CASE(L"container list --format table", L"list", true)
57 +COMMAND_LINE_TEST_CASE(L"container list --format badformat", L"list", false)
58 +COMMAND_LINE_TEST_CASE(L"run ubuntu", L"run", true)
59 +COMMAND_LINE_TEST_CASE(L"run --rm -it --entrypoint bash archlinux:latest -c \"echo 123\"", L"run", true)
60 +COMMAND_LINE_TEST_CASE(L"run --rm --entrypoint /bin/bash debian:latest -c ls", L"run", true)
61 +COMMAND_LINE_TEST_CASE(L"run jrottenberg/ffmpeg:4.4-alpine -i http://url/to/media.mp4 -stats", L"run", true)
62 +COMMAND_LINE_TEST_CASE(
63 + L"run -v ./:/data jrottenberg/ffmpeg:4.4-scratch -stats -i http://www.hevc-10bit.mkv -c:v libx265 -pix_fmt yuv420p10 -t "
64 + L"5 -f mp4 test.mp4",
65 + L"run",
66 + true)
67 +COMMAND_LINE_TEST_CASE(
68 + L"run -v ./:/data -it jrottenberg/ffmpeg:4.4-scratch -stats -i https://file-examples/file_example_MP4_480_1_5MG.mp4 -c:v "
69 + L"libx265 -pix_fmt yuv420p10 -t 5 -f mp4 /dataout.mp4",
70 + L"run",
71 + true)
72 +COMMAND_LINE_TEST_CASE(L"container run ubuntu bash -c 'echo Hello World'", L"run", true)
73 +COMMAND_LINE_TEST_CASE(L"container run ubuntu", L"run", true)
74 +COMMAND_LINE_TEST_CASE(L"container run -it --name foo ubuntu", L"run", true)
75 +COMMAND_LINE_TEST_CASE(L"container run --rm -it --name foo ubuntu", L"run", true)
76 +COMMAND_LINE_TEST_CASE(L"stop", L"stop", true)
77 +COMMAND_LINE_TEST_CASE(L"container stop cont1 --signal 9", L"stop", true)
78 +COMMAND_LINE_TEST_CASE(L"container stop cont1 --signal SIGALRM", L"stop", true)
79 +COMMAND_LINE_TEST_CASE(L"container stop cont1 --signal sigkill", L"stop", true)
80 +COMMAND_LINE_TEST_CASE(L"container stop cont1 -s KILL", L"stop", true)
81 +COMMAND_LINE_TEST_CASE(L"start cont", L"start", true)
82 +COMMAND_LINE_TEST_CASE(L"container start cont", L"start", true)
83 +COMMAND_LINE_TEST_CASE(L"container start --attach cont", L"start", true)
84 +COMMAND_LINE_TEST_CASE(L"container start -a cont", L"start", true)
85 +COMMAND_LINE_TEST_CASE(L"create ubuntu:latest", L"create", true)
86 +COMMAND_LINE_TEST_CASE(L"container create --name foo ubuntu", L"create", true)
87 +COMMAND_LINE_TEST_CASE(L"create --workdir /app ubuntu", L"create", true)
88 +COMMAND_LINE_TEST_CASE(L"create -w /app ubuntu", L"create", true)
89 +COMMAND_LINE_TEST_CASE(L"container create --workdir /app ubuntu sh", L"create", true)
90 +COMMAND_LINE_TEST_CASE(L"create --workdir", L"create", false) // Missing value for --workdir
91 +COMMAND_LINE_TEST_CASE(L"create --workdir \"\" ubuntu", L"create", false) // Empty working directory
92 +COMMAND_LINE_TEST_CASE(L"run --workdir /app ubuntu echo hello", L"run", true)
93 +COMMAND_LINE_TEST_CASE(L"run -w /app ubuntu echo hello", L"run", true)
94 +COMMAND_LINE_TEST_CASE(L"container run --workdir /app ubuntu sh", L"run", true)
95 +COMMAND_LINE_TEST_CASE(L"run --workdir", L"run", false) // Missing value for --workdir
96 +COMMAND_LINE_TEST_CASE(L"run --workdir \"\" ubuntu echo hello", L"run", false) // Empty working directory
97 +// DNS tests for container create
98 +COMMAND_LINE_TEST_CASE(L"create --dns 1.1.1.1 ubuntu", L"create", true)
99 +COMMAND_LINE_TEST_CASE(L"create --dns 1.1.1.1 --dns 8.8.8.8 ubuntu", L"create", true) // Multiple --dns values
100 +COMMAND_LINE_TEST_CASE(L"container create --dns-search example.com ubuntu", L"create", true)
101 +COMMAND_LINE_TEST_CASE(L"container create --dns-search example.com --dns-search test.local ubuntu", L"create", true) // Multiple --dns-search values
102 +COMMAND_LINE_TEST_CASE(L"create --dns-option ndots:5 ubuntu", L"create", true)
103 +COMMAND_LINE_TEST_CASE(L"create --dns-option ndots:5 --dns-option timeout:3 ubuntu", L"create", true) // Multiple --dns-option values
104 +COMMAND_LINE_TEST_CASE(L"create --dns 1.1.1.1 --dns-search example.com --dns-option ndots:5 ubuntu", L"create", true) // Combined DNS options
105 +COMMAND_LINE_TEST_CASE(L"create --dns", L"create", false) // Missing value for --dns
106 +COMMAND_LINE_TEST_CASE(L"create --dns-search", L"create", false) // Missing value for --dns-search
107 +COMMAND_LINE_TEST_CASE(L"create --dns-option", L"create", false) // Missing value for --dns-option
108 +// DNS tests for container run
109 +COMMAND_LINE_TEST_CASE(L"run --dns 1.1.1.1 ubuntu", L"run", true)
110 +COMMAND_LINE_TEST_CASE(L"run --dns 1.1.1.1 --dns 8.8.8.8 ubuntu", L"run", true) // Multiple --dns values
111 +COMMAND_LINE_TEST_CASE(L"container run --dns-search example.com ubuntu", L"run", true)
112 +COMMAND_LINE_TEST_CASE(L"container run --dns-search example.com --dns-search test.local ubuntu", L"run", true) // Multiple --dns-search values
113 +COMMAND_LINE_TEST_CASE(L"run --dns-option ndots:5 ubuntu", L"run", true)
114 +COMMAND_LINE_TEST_CASE(L"run --dns-option ndots:5 --dns-option timeout:3 ubuntu", L"run", true) // Multiple --dns-option values
115 +COMMAND_LINE_TEST_CASE(L"run --dns 1.1.1.1 --dns-search example.com --dns-option ndots:5 ubuntu", L"run", true) // Combined DNS options
116 +COMMAND_LINE_TEST_CASE(L"run --dns", L"run", false) // Missing value for --dns
117 +COMMAND_LINE_TEST_CASE(L"run --dns-search", L"run", false) // Missing value for --dns-search
118 +COMMAND_LINE_TEST_CASE(L"run --dns-option", L"run", false) // Missing value for --dns-option
119 +COMMAND_LINE_TEST_CASE(L"exec cont1 echo Hello", L"exec", true)
120 +COMMAND_LINE_TEST_CASE(L"exec cont1", L"exec", false) // Missing required command argument
121 +COMMAND_LINE_TEST_CASE(L"container exec -it cont1 sh -c \"echo a && echo b\"", L"exec", true) // docker exec example
122 +COMMAND_LINE_TEST_CASE(L"exec --workdir /app cont1 echo Hello", L"exec", true)
123 +COMMAND_LINE_TEST_CASE(L"exec -w /app cont1 echo Hello", L"exec", true)
124 +COMMAND_LINE_TEST_CASE(L"container exec --workdir /app cont1 sh", L"exec", true)
125 +COMMAND_LINE_TEST_CASE(L"exec --workdir", L"exec", false) // Missing value for --workdir
126 +COMMAND_LINE_TEST_CASE(L"exec --workdir \"\" cont1 echo Hello", L"exec", false) // Empty working directory
127 +COMMAND_LINE_TEST_CASE(L"kill cont1 --signal sigkill", L"kill", true)
128 +COMMAND_LINE_TEST_CASE(L"container kill cont1 -s KILL", L"kill", true)
129 +COMMAND_LINE_TEST_CASE(L"inspect cont1", L"inspect", true)
130 +COMMAND_LINE_TEST_CASE(L"container inspect cont1", L"inspect", true)
131 +COMMAND_LINE_TEST_CASE(L"remove cont1", L"remove", true)
132 +COMMAND_LINE_TEST_CASE(L"container remove cont1 cont2", L"remove", true)
133 +COMMAND_LINE_TEST_CASE(L"rm cont1", L"remove", true)
134 +COMMAND_LINE_TEST_CASE(L"container rm cont1 cont2", L"remove", true)
135 +COMMAND_LINE_TEST_CASE(L"container attach cont", L"attach", true)
136 +COMMAND_LINE_TEST_CASE(L"container attach", L"attach", false)
137 +
138 +// Logs command
139 +COMMAND_LINE_TEST_CASE(L"logs cont1", L"logs", true)
140 +COMMAND_LINE_TEST_CASE(L"container logs cont1", L"logs", true)
141 +COMMAND_LINE_TEST_CASE(L"container logs --follow cont1", L"logs", true)
142 +COMMAND_LINE_TEST_CASE(L"container logs cont1 -f", L"logs", true)
143 +COMMAND_LINE_TEST_CASE(L"container logs", L"logs", false)
144 +
145 +// Image command
146 +COMMAND_LINE_TEST_CASE(L"image build C:\\context", L"build", true)
147 +COMMAND_LINE_TEST_CASE(L"image build C:\\context --tag test:latest", L"build", true)
148 +COMMAND_LINE_TEST_CASE(L"image build C:\\context -t test", L"build", true)
149 +COMMAND_LINE_TEST_CASE(L"image build C:\\context --file Dockerfile.custom", L"build", true)
150 +COMMAND_LINE_TEST_CASE(L"image build C:\\context -f -", L"build", true)
151 +COMMAND_LINE_TEST_CASE(L"image build C:\\context -t test:latest -f Dockerfile.other", L"build", true)
152 +COMMAND_LINE_TEST_CASE(L"image build C:\\context -t tag1 -t tag2", L"build", true)
153 +COMMAND_LINE_TEST_CASE(L"image build C:\\context --tag tag1 --tag tag2 --tag tag3", L"build", true)
154 +COMMAND_LINE_TEST_CASE(L"image build C:\\context --build-arg KEY=VALUE", L"build", true)
155 +COMMAND_LINE_TEST_CASE(L"image build C:\\context --build-arg A=1 --build-arg B=2", L"build", true)
156 +COMMAND_LINE_TEST_CASE(L"image build C:\\context -t test:latest --build-arg KEY=VALUE -f Dockerfile.custom", L"build", true)
157 +COMMAND_LINE_TEST_CASE(L"image build C:\\context --verbose", L"build", true)
158 +COMMAND_LINE_TEST_CASE(L"image build C:\\context -t test --build-arg KEY=VALUE --verbose", L"build", true)
159 +COMMAND_LINE_TEST_CASE(L"image build C:\\context --no-cache", L"build", true)
160 +COMMAND_LINE_TEST_CASE(L"image build C:\\context --no-cache --verbose", L"build", true)
161 +COMMAND_LINE_TEST_CASE(L"image build C:\\context -t test --no-cache", L"build", true)
162 +COMMAND_LINE_TEST_CASE(L"image build", L"build", false)
163 +COMMAND_LINE_TEST_CASE(L"build C:\\context", L"build", true)
164 +COMMAND_LINE_TEST_CASE(L"build C:\\context -t test", L"build", true)
165 +COMMAND_LINE_TEST_CASE(L"image list", L"list", true)
166 +COMMAND_LINE_TEST_CASE(L"image list --no-trunc", L"list", true)
167 +COMMAND_LINE_TEST_CASE(L"images", L"images", true) // Aliased off the root changes the name
168 +COMMAND_LINE_TEST_CASE(L"image ls", L"list", true)
169 +COMMAND_LINE_TEST_CASE(L"image list --format json", L"list", true)
170 +COMMAND_LINE_TEST_CASE(L"image list --format badformat", L"list", false)
171 +COMMAND_LINE_TEST_CASE(L"image list --verbose", L"list", true)
172 +COMMAND_LINE_TEST_CASE(L"image list -q", L"list", true)
173 +COMMAND_LINE_TEST_CASE(L"image pull ubuntu", L"pull", true)
174 +COMMAND_LINE_TEST_CASE(L"pull ubuntu", L"pull", true)
175 +
176 +// Version command tests
177 +COMMAND_LINE_TEST_CASE(L"version", L"version", true)
178 +COMMAND_LINE_TEST_CASE(L"version --help", L"version", true)
179 +COMMAND_LINE_TEST_CASE(L"version extraarg", L"version", false)
180 +// Settings command
181 +COMMAND_LINE_TEST_CASE(L"settings", L"settings", true)
182 +COMMAND_LINE_TEST_CASE(L"settings reset", L"reset", true)
183 +
184 +// Error cases
185 +COMMAND_LINE_TEST_CASE(L"invalid command", L"", false)
186 +COMMAND_LINE_TEST_CASE(L"CONTAINER list", L"list", false) // We are intentionally case-sensitive
187 +COMMAND_LINE_TEST_CASE(L"container LS", L"list", false) // commands and aliases are case-sensitive
188 +COMMAND_LINE_TEST_CASE(L"container list --FORMAT json", L"list", false) // Args also case-sensitive
189 +COMMAND_LINE_TEST_CASE(L"container list -A", L"list", false) // So are arg aliases
test/windows/wslc/ParserTestCases.h new
+141
@@ -0,0 +1,141 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ParserTestCases.h
8 +
9 +Abstract:
10 +
11 + X-macro definitions for WSLC CLI parser test cases.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "Argument.h"
18 +#include "ArgumentTypes.h"
19 +#include <string>
20 +#include <vector>
21 +
22 +// ArgumentSet enum - defines which set of arguments to use for parsing
23 +enum class ArgumentSet
24 +{
25 + Run,
26 + List,
27 +};
28 +
29 +// ParserTestCase - represents a single test case
30 +struct ParserTestCase
31 +{
32 + ArgumentSet argumentSet;
33 + bool expectedResult;
34 + std::wstring commandLine;
35 +};
36 +
37 +// Function to get the argument definitions for a given ArgumentSet
38 +inline std::vector<wsl::windows::wslc::Argument> GetArgumentsForSet(ArgumentSet argumentSet)
39 +{
40 + using namespace wsl::windows::wslc;
41 + using namespace wsl::windows::wslc::argument;
42 +
43 + switch (argumentSet)
44 + {
45 + case ArgumentSet::Run:
46 + return {
47 + Argument::Create(ArgType::ImageId, true), // Required positional argument
48 + Argument::Create(ArgType::Command, false), // Optional positional argument
49 + Argument::Create(ArgType::ForwardArgs, false),
50 + Argument::Create(ArgType::Help),
51 + Argument::Create(ArgType::Interactive),
52 + Argument::Create(ArgType::Verbose),
53 + Argument::Create(ArgType::Remove),
54 + Argument::Create(ArgType::Signal),
55 + Argument::Create(ArgType::Time),
56 + Argument::Create(ArgType::Publish, false, NO_LIMIT), // Not required, unlimited.
57 + };
58 +
59 + case ArgumentSet::List:
60 + return {
61 + Argument::Create(ArgType::ContainerId, false, NO_LIMIT), // Optional positional
62 + Argument::Create(ArgType::Help),
63 + Argument::Create(ArgType::Verbose),
64 + };
65 +
66 + default:
67 + return {};
68 + }
69 +}
70 +// X-macro format: WSLC_PARSER_TEST_CASE(ArgumentSetValue, ExpectedResult, CommandLine)
71 +// ArgumentSetValue: Just the enum value name (e.g., Run), without ArgumentSet:: prefix
72 +// ExpectedResult: true if test should succeed, false if it should fail
73 +// CommandLine: The command line string to test
74 +
75 +// clang-format off
76 +#define WSLC_PARSER_TEST_CASES \
77 +/* Simple case with required arg and simple other args */ \
78 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -?)") \
79 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1)") \
80 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --verbose image1)") \
81 +\
82 +/* Value tests, flag and non-flag, multi-value */ \
83 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --publish=80:80 image1)") \
84 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --publish 80:80 image1)") \
85 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p=80:80 image1)") \
86 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p 80:80 image1)") \
87 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p 80:80 -p 443:443 image1)") \
88 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p=80:80 -p=443:443 image1)") \
89 +WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --verbose --verbose image1)") \
90 +\
91 +/* Flag parse tests */ \
92 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -? image1)") \
93 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -?i image1)") \
94 +WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -i?p- image1)") \
95 +WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pi? image1)") \
96 +WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pi?=80:80 image1)") \
97 +WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pi? 80:80 image1)") \
98 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -i?p 80:80 image1)") \
99 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -i?p=80:80 image1)") \
100 +\
101 +/* Validation tests */ \
102 +WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --signal FOO image1)") \
103 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --signal 9 image1)") \
104 +WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -t blah image1)") \
105 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -t 5 image1)") \
106 +\
107 +/* Multi-positional tests */ \
108 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command)") \
109 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command --f -z forward hello world)") \
110 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command forward hello world)") \
111 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command forward"hello world")") \
112 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command f="hello world" forward echo)") \
113 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --verbose image1 command f="hello world" forward echo)") \
114 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 \\command\\?"" --f -z forward hello world)") \
115 +\
116 +/* Once the image name is parsed, the next token becomes the optional <command> positional \
117 + * and everything after that goes into ForwardArgs. Neither <command> nor ForwardArgs are \
118 + * interpreted as wslc options. The second case uses '\' + newline between tokens, which \
119 + * CommandLineToArgvW passes through as literal '\' tokens that the container shell \
120 + * handles correctly. */ \
121 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc jrottenberg/ffmpeg:4.4-alpine ffmpeg -i http://url/to/media.mp4 -stats)") \
122 +WSLC_PARSER_TEST_CASE(Run, true, L"wslc jrottenberg/ffmpeg:4.4-alpine \\\nffmpeg \\\n-i http://url/to/media.mp4 \\\n-stats") \
123 +\
124 +/* List cases with multiple args and flags that can come after the optional multi-positional. */ \
125 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc)") \
126 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1)") \
127 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 cont2)") \
128 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc --verbose cont1)") \
129 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc --verbose cont1 cont2)") \
130 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 --verbose cont2)") \
131 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 cont2 --verbose)") \
132 +\
133 +/* Failure List cases */ \
134 +WSLC_PARSER_TEST_CASE(List, false, LR"(wslc --invalidarg)") \
135 +WSLC_PARSER_TEST_CASE(List, false, LR"(wslc --invalidarg cont1)") \
136 +WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -i cont1 cont2)") \
137 +WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -vp cont1)") \
138 +WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 -v cont2 -12)") \
139 +WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 --verbose=false cont2)") \
140 +WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 cont2 --invalidarg)")
141 +// clang-format on
\ No newline at end of file
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp new
+214
@@ -0,0 +1,214 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLIArgumentUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC CLI argument parsing and validation.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "WSLCCLITestHelpers.h"
18 +
19 +#include "Argument.h"
20 +#include "ArgumentTypes.h"
21 +#include "ArgumentValidation.h"
22 +#include "Exceptions.h"
23 +#include <wslc.h>
24 +
25 +using namespace wsl::windows::wslc;
26 +using namespace wsl::windows::wslc::argument;
27 +
28 +using namespace WSLCTestHelpers;
29 +using namespace WEX::Logging;
30 +using namespace WEX::Common;
31 +using namespace WEX::TestExecution;
32 +
33 +namespace WSLCCLIArgumentUnitTests {
34 +class WSLCCLIArgumentUnitTests
35 +{
36 + WSLC_TEST_CLASS(WSLCCLIArgumentUnitTests)
37 +
38 + TEST_CLASS_SETUP(TestClassSetup)
39 + {
40 + // Add any necessary setup for argument tests
41 + return true;
42 + }
43 +
44 + TEST_CLASS_CLEANUP(TestClassCleanup)
45 + {
46 + // Add any necessary cleanup for argument tests
47 + return true;
48 + }
49 +
50 + // Test: Verify Argument::Create() successfully creates arguments for all ArgType enum values
51 + TEST_METHOD(ArgumentCreate_AllArguments)
52 + {
53 + // ArgMap is the container for processed args.
54 + ArgMap args;
55 +
56 + // Iterate through all ArgType enum values except Max
57 + auto allArgTypes = std::vector<ArgType>{};
58 + for (int i = 0; i < static_cast<int>(ArgType::Max); ++i)
59 + {
60 + ArgType argType = static_cast<ArgType>(i);
61 +
62 + // Create argument using Create
63 + Argument arg = Argument::Create(argType);
64 +
65 + // Verify the argument was created successfully by checking its type matches
66 + VERIFY_ARE_EQUAL(static_cast<int>(arg.Type()), i);
67 +
68 + // Verify the argument has basic properties set
69 + // (Name should not be empty for valid argument types)
70 + VERIFY_IS_FALSE(arg.Name().empty());
71 + LogComment(L"Verified Argument::Create() creates argument with name: " + arg.Name());
72 +
73 + // Add the argument to the ArgMap with a test value based on its type.
74 + VERIFY_IS_FALSE(args.Contains(argType));
75 + switch (arg.Kind())
76 + {
77 + case Kind::Value:
78 + case Kind::Positional:
79 + args.Add(argType, std::wstring(L"test"));
80 + break;
81 + case Kind::Forward:
82 + args.Add(argType, std::vector<std::wstring>{L"forward1", L"forward2"});
83 + break;
84 + case Kind::Flag:
85 + args.Add(argType, true);
86 + break;
87 + default:
88 + VERIFY_FAIL(L"Unhandled ValueType in test");
89 + }
90 +
91 + allArgTypes.push_back(argType);
92 + VERIFY_IS_TRUE(args.Contains(argType));
93 + }
94 +
95 + // We do not have a runtime Get for argument values, so we will instead use the keys
96 + // in the argmap. The fact that the keys exist and can be used to retrieve values
97 + // verifies that Argument::Create() created arguments that are compatible with ArgMap.
98 + // Verify all created argument types are in the ArgMap keys
99 + auto argMapKeys = args.GetKeys();
100 + VERIFY_ARE_EQUAL(argMapKeys.size(), allArgTypes.size());
101 + for (const auto& argType : allArgTypes)
102 + {
103 + VERIFY_IS_TRUE(std::find(argMapKeys.begin(), argMapKeys.end(), argType) != argMapKeys.end());
104 + }
105 + }
106 +
107 + // Test: Verify Argument::Create() successfully creates arguments for all ArgType enum values
108 + TEST_METHOD(ArgumentValidation_ValueValidation)
109 + {
110 + // Verify integer conversion for supported types.
111 + auto longlong = validation::GetIntegerFromString<LONGLONG>(L"1234567890123");
112 + VERIFY_ARE_EQUAL(longlong, 1234567890123LL);
113 + VERIFY_THROWS(validation::GetIntegerFromString<LONGLONG>(L"abc"), ArgumentException); // Not a number
114 + VERIFY_THROWS(validation::GetIntegerFromString<LONGLONG>(L"-92233720369999854775808"), ArgumentException); // Out of range
115 + VERIFY_NO_THROW(validation::ValidateIntegerFromString<LONGLONG>({L"1234", L"-1234567890123"}, L"testArg"));
116 + VERIFY_THROWS(validation::ValidateIntegerFromString<LONGLONG>({L"1234", L"-92233720369999854775808"}, L"testArg"), ArgumentException);
117 +
118 + // Verify WSLCSignal conversion
119 + auto validSignal = validation::GetWSLCSignalFromString(L"SIGTERM");
120 + VERIFY_ARE_EQUAL(validSignal, WSLCSignalSIGTERM);
121 + validSignal = validation::GetWSLCSignalFromString(L"TERM"); // No prefix
122 + VERIFY_ARE_EQUAL(validSignal, WSLCSignalSIGTERM);
123 + validSignal = validation::GetWSLCSignalFromString(L"sIgTerm"); // Case-insensitive
124 + VERIFY_ARE_EQUAL(validSignal, WSLCSignalSIGTERM);
125 + validSignal = validation::GetWSLCSignalFromString(L"term"); // Case-insensitive no prefix
126 + VERIFY_ARE_EQUAL(validSignal, WSLCSignalSIGTERM);
127 + VERIFY_THROWS(validation::GetWSLCSignalFromString(L"INVALID_SIGNAL"), ArgumentException);
128 + validSignal = validation::GetWSLCSignalFromString(L"15"); // SIGTERM is 15
129 + VERIFY_ARE_EQUAL(validSignal, WSLCSignalSIGTERM);
130 + VERIFY_THROWS(validation::GetWSLCSignalFromString(L"999"), ArgumentException); // Out of range
131 + VERIFY_NO_THROW(validation::ValidateWSLCSignalFromString({L"HUP", L"9", L"SIGKILL", L"stop"}, L"signalArg"));
132 + VERIFY_THROWS(validation::ValidateWSLCSignalFromString({L"SIGHUP", L"999"}, L"signalArg"), ArgumentException); // 999 is out of range
133 +
134 + // Verify format type
135 + auto format = validation::GetFormatTypeFromString(L"json");
136 + VERIFY_ARE_EQUAL(format, FormatType::Json);
137 + format = validation::GetFormatTypeFromString(L"table");
138 + VERIFY_ARE_EQUAL(format, FormatType::Table);
139 + VERIFY_THROWS(validation::GetFormatTypeFromString(L"xml"), ArgumentException);
140 + VERIFY_NO_THROW(validation::ValidateFormatTypeFromString({L"json", L"table"}, L"formatArg"));
141 + VERIFY_THROWS(validation::ValidateFormatTypeFromString({L"JSON", L"TABLE", L"csv"}, L"formatArg"), ArgumentException);
142 + }
143 +
144 + // Test: Verify EnumVariantMap behavior with ArgTypes.
145 + TEST_METHOD(EnumVariantMap_AllDataTypes)
146 + {
147 + // ArgMap is an EnumVariantMap
148 + ArgMap argsContainer;
149 +
150 + // Verify basic add
151 + argsContainer.Add<ArgType::Help>(true);
152 + VERIFY_IS_TRUE(argsContainer.Contains(ArgType::Help));
153 + argsContainer.Add<ArgType::ContainerId>(std::wstring(L"test"));
154 + VERIFY_IS_TRUE(argsContainer.Contains(ArgType::ContainerId));
155 + argsContainer.Add<ArgType::ForwardArgs>(std::vector<std::wstring>{L"test1", L"test2"});
156 + VERIFY_IS_TRUE(argsContainer.Contains(ArgType::ForwardArgs));
157 +
158 + // Verify basic retrieval
159 + auto retrievedBool = argsContainer.Get<ArgType::Help>();
160 + VERIFY_ARE_EQUAL(retrievedBool, true);
161 + auto retrievedString = argsContainer.Get<ArgType::ContainerId>();
162 + VERIFY_ARE_EQUAL(retrievedString, std::wstring(L"test"));
163 + auto retrievedStringSet = argsContainer.Get<ArgType::ForwardArgs>();
164 + VERIFY_ARE_EQUAL(retrievedStringSet[0], std::wstring(L"test1"));
165 + VERIFY_ARE_EQUAL(retrievedStringSet[1], std::wstring(L"test2"));
166 +
167 + // Verify multimap functionality and Runtime Add
168 + argsContainer.Add(ArgType::Publish, std::wstring(L"test1"));
169 + argsContainer.Add(ArgType::Publish, std::wstring(L"test2"));
170 + argsContainer.Add(ArgType::Publish, std::wstring(L"test3"));
171 + VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 3);
172 + auto publishArgs = argsContainer.GetAll<ArgType::Publish>();
173 + VERIFY_ARE_EQUAL(publishArgs.size(), 3);
174 + VERIFY_ARE_EQUAL(publishArgs[0], std::wstring(L"test1"));
175 + VERIFY_ARE_EQUAL(publishArgs[1], std::wstring(L"test2"));
176 + VERIFY_ARE_EQUAL(publishArgs[2], std::wstring(L"test3"));
177 +
178 + // Verify Remove
179 + argsContainer.Remove(ArgType::Publish);
180 + VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 0);
181 +
182 + // Verify compile time add works like runtime add for multimap types.
183 + argsContainer.Add<ArgType::Publish>(L"test1");
184 + argsContainer.Add<ArgType::Publish>(L"test2");
185 + argsContainer.Add<ArgType::Publish>(L"test3");
186 + VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 3);
187 + publishArgs = argsContainer.GetAll<ArgType::Publish>();
188 + VERIFY_ARE_EQUAL(publishArgs.size(), 3);
189 + VERIFY_ARE_EQUAL(publishArgs[0], std::wstring(L"test1"));
190 + VERIFY_ARE_EQUAL(publishArgs[1], std::wstring(L"test2"));
191 + VERIFY_ARE_EQUAL(publishArgs[2], std::wstring(L"test3"));
192 +
193 + // Verify Keys
194 + auto allArgTypes = argsContainer.GetKeys();
195 + VERIFY_ARE_EQUAL(allArgTypes.size(), 4);
196 + VERIFY_IS_TRUE(std::find(allArgTypes.begin(), allArgTypes.end(), ArgType::Help) != allArgTypes.end());
197 + VERIFY_IS_TRUE(std::find(allArgTypes.begin(), allArgTypes.end(), ArgType::ContainerId) != allArgTypes.end());
198 + VERIFY_IS_TRUE(std::find(allArgTypes.begin(), allArgTypes.end(), ArgType::Publish) != allArgTypes.end());
199 + VERIFY_IS_TRUE(std::find(allArgTypes.begin(), allArgTypes.end(), ArgType::ForwardArgs) != allArgTypes.end());
200 +
201 + // Verify count
202 + VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Help), 1);
203 + VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::ContainerId), 1);
204 + VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 3);
205 + VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::ForwardArgs), 1);
206 + VERIFY_ARE_EQUAL(argsContainer.GetCount(), 6); // 1 Help + 1 ContainerId + 3 Publish + 1 ForwardArgs
207 + argsContainer.Remove(ArgType::Help);
208 + argsContainer.Remove(ArgType::ContainerId);
209 + argsContainer.Remove(ArgType::Publish);
210 + argsContainer.Remove(ArgType::ForwardArgs);
211 + VERIFY_ARE_EQUAL(argsContainer.GetCount(), 0);
212 + }
213 +};
214 +} // namespace WSLCCLIArgumentUnitTests
\ No newline at end of file
test/windows/wslc/WSLCCLICommandUnitTests.cpp new
+261
@@ -0,0 +1,261 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLICommandUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC CLI Command classes.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include <unordered_map>
17 +#include <unordered_set>
18 +#include "windows/Common.h"
19 +#include "WSLCCLITestHelpers.h"
20 +
21 +#include "Command.h"
22 +#include "RootCommand.h"
23 +#include "ContainerCommand.h"
24 +#include "SessionCommand.h"
25 +#include "VersionCommand.h"
26 +
27 +using namespace wsl::windows::wslc;
28 +using namespace WSLCTestHelpers;
29 +using namespace WEX::Logging;
30 +using namespace WEX::Common;
31 +using namespace WEX::TestExecution;
32 +
33 +namespace WSLCCLICommandUnitTests {
34 +class WSLCCLICommandUnitTests
35 +{
36 + WSLC_TEST_CLASS(WSLCCLICommandUnitTests)
37 +
38 + TEST_CLASS_SETUP(TestClassSetup)
39 + {
40 + Log::Comment(L"WSLC CLI Command Unit Tests - Class Setup");
41 + return true;
42 + }
43 +
44 + TEST_CLASS_CLEANUP(TestClassCleanup)
45 + {
46 + Log::Comment(L"WSLC CLI Command Unit Tests - Class Cleanup");
47 + return true;
48 + }
49 +
50 + // Test: Verify RootCommand has subcommands
51 + TEST_METHOD(RootCommand_HasSubcommands)
52 + {
53 + auto cmd = RootCommand();
54 +
55 + auto subcommands = cmd.GetCommands();
56 +
57 + // Verify it has subcommands
58 + VERIFY_IS_TRUE(subcommands.size() > 0);
59 + LogComment(L"RootCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
60 +
61 + // Verify each subcommand is valid
62 + for (const auto& subcmd : subcommands)
63 + {
64 + VERIFY_IS_NOT_NULL(subcmd.get());
65 + }
66 + }
67 +
68 + // Test: Verify SessionCommand has subcommands
69 + TEST_METHOD(SessionCommand_HasSubcommands)
70 + {
71 + auto cmd = SessionCommand(L"session");
72 + auto subcommands = cmd.GetCommands();
73 +
74 + // Verify it has subcommands
75 + VERIFY_IS_TRUE(subcommands.size() > 0);
76 + LogComment(L"SessionCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
77 +
78 + // Log subcommand types
79 + for (const auto& subcmd : subcommands)
80 + {
81 + VERIFY_IS_NOT_NULL(subcmd.get());
82 + }
83 + }
84 +
85 + // Test: Verify SessionEnterCommand has the expected arguments
86 + TEST_METHOD(SessionEnterCommand_HasExpectedArguments)
87 + {
88 + auto cmd = SessionEnterCommand(L"session");
89 + auto args = cmd.GetArguments();
90 +
91 + // Should have 2 arguments: storage-path (positional, required) and name (value, optional)
92 + VERIFY_ARE_EQUAL(2u, args.size());
93 +
94 + // Verify storage-path argument
95 + auto& storagePath = args[0];
96 + VERIFY_ARE_EQUAL(ArgType::StoragePath, storagePath.Type());
97 + VERIFY_ARE_EQUAL(Kind::Positional, storagePath.Kind());
98 + VERIFY_IS_TRUE(storagePath.Required());
99 +
100 + // Verify name argument
101 + auto& name = args[1];
102 + VERIFY_ARE_EQUAL(ArgType::Name, name.Type());
103 + VERIFY_ARE_EQUAL(Kind::Value, name.Kind());
104 + VERIFY_IS_FALSE(name.Required());
105 + }
106 +
107 + // Test: Verify SessionEnterCommand descriptions are not empty
108 + TEST_METHOD(SessionEnterCommand_HasDescriptions)
109 + {
110 + auto cmd = SessionEnterCommand(L"session");
111 +
112 + VERIFY_IS_FALSE(cmd.ShortDescription().empty());
113 + VERIFY_IS_FALSE(cmd.LongDescription().empty());
114 + }
115 +
116 + // Test: Verify ContainerCommand has subcommands
117 + TEST_METHOD(ContainerCommand_HasSubcommands)
118 + {
119 + auto cmd = ContainerCommand(L"container");
120 + auto subcommands = cmd.GetCommands();
121 +
122 + // Verify it has subcommands
123 + VERIFY_IS_TRUE(subcommands.size() > 0);
124 + LogComment(L"ContainerCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
125 +
126 + // Log subcommand types
127 + for (const auto& subcmd : subcommands)
128 + {
129 + VERIFY_IS_NOT_NULL(subcmd.get());
130 + }
131 + }
132 +
133 + // Test: Verify VersionCommand has the correct name
134 + TEST_METHOD(VersionCommand_HasCorrectName)
135 + {
136 + auto cmd = VersionCommand(L"wslc");
137 + VERIFY_ARE_EQUAL(std::wstring_view(L"version"), cmd.Name());
138 + }
139 +
140 + // Test: Verify VersionCommand has no subcommands
141 + TEST_METHOD(VersionCommand_HasNoSubcommands)
142 + {
143 + auto cmd = VersionCommand(L"wslc");
144 + VERIFY_ARE_EQUAL(0u, cmd.GetCommands().size());
145 + }
146 +
147 + // Test: Verify VersionCommand has no arguments (only the auto-added --help)
148 + TEST_METHOD(VersionCommand_HasNoArguments)
149 + {
150 + auto cmd = VersionCommand(L"wslc");
151 + VERIFY_ARE_EQUAL(0u, cmd.GetArguments().size());
152 + // Test out that auto added help command is the only one
153 + VERIFY_ARE_EQUAL(1u, cmd.GetAllArguments().size());
154 + }
155 +
156 + // Test: Verify RootCommand contains VersionCommand as a subcommand
157 + TEST_METHOD(RootCommand_ContainsVersionCommand)
158 + {
159 + auto root = RootCommand();
160 + auto subcommands = root.GetCommands();
161 +
162 + bool found = false;
163 + for (const auto& subcmd : subcommands)
164 + {
165 + if (subcmd->Name() == VersionCommand::CommandName)
166 + {
167 + found = true;
168 + break;
169 + }
170 + }
171 +
172 + VERIFY_IS_TRUE(found, L"RootCommand should contain VersionCommand");
173 + }
174 +
175 + // Walk every command in the root tree and verify no argument collisions.
176 + TEST_METHOD(AllCommands_NoAmbiguousArgumentNamesOrAliases)
177 + {
178 + // Build a lookup table from ArgType -> enum name string using the same X-macro.
179 + static constexpr const wchar_t* c_argTypeNames[] = {
180 +#define WSLC_ARG_ENUM(EnumName, Name, Alias, Kind, Desc) L## #EnumName,
181 + WSLC_ARGUMENTS(WSLC_ARG_ENUM)
182 +#undef WSLC_ARG_ENUM
183 + };
184 +
185 + const auto ArgTypeName = [](argument::ArgType type) -> std::wstring_view {
186 + const auto index = static_cast<size_t>(type);
187 + const auto max = static_cast<size_t>(argument::ArgType::Max);
188 + if (index < max)
189 + {
190 + return c_argTypeNames[index];
191 + }
192 +
193 + return L"<unknown>";
194 + };
195 +
196 + // Starting with the Root command, verify no argument collisions.
197 + std::vector<std::unique_ptr<Command>> commands;
198 + commands.push_back(std::make_unique<RootCommand>());
199 +
200 + while (!commands.empty())
201 + {
202 + auto current = std::move(commands.back());
203 + commands.pop_back();
204 + VERIFY_IS_NOT_NULL(current.get());
205 +
206 + const std::wstring commandFullName(current->FullName());
207 + std::unordered_set<size_t> seenTypes;
208 + std::unordered_map<std::wstring, argument::ArgType> seenNames;
209 + std::unordered_map<std::wstring, argument::ArgType> seenAliases;
210 +
211 + for (const auto& arg : current->GetAllArguments())
212 + {
213 + // Check for duplicate ArgType registration.
214 + if (!seenTypes.emplace(static_cast<size_t>(arg.Type())).second)
215 + {
216 + VERIFY_FAIL(std::format(L"Command '{}' registers ArgType '{}' more than once", commandFullName, ArgTypeName(arg.Type()))
217 + .c_str());
218 + }
219 +
220 + // Check name collision between distinct ArgTypes.
221 + const auto& name = arg.Name();
222 + auto [nameIt, nameInserted] = seenNames.emplace(name, arg.Type());
223 + if (!nameInserted)
224 + {
225 + VERIFY_FAIL(std::format(
226 + L"Command '{}' has duplicate name '--{}' (ArgType '{}' conflicts with ArgType '{}')",
227 + commandFullName,
228 + name,
229 + ArgTypeName(arg.Type()),
230 + ArgTypeName(nameIt->second))
231 + .c_str());
232 + }
233 +
234 + // Check alias collision between distinct ArgTypes; skip empty aliases (NO_ALIAS).
235 + const auto& alias = arg.Alias();
236 + if (!alias.empty())
237 + {
238 + auto [aliasIt, aliasInserted] = seenAliases.emplace(alias, arg.Type());
239 + if (!aliasInserted)
240 + {
241 + VERIFY_FAIL(std::format(
242 + L"Command '{}' has duplicate alias '-{}' (ArgType '{}' conflicts with ArgType '{}')",
243 + commandFullName,
244 + alias,
245 + ArgTypeName(arg.Type()),
246 + ArgTypeName(aliasIt->second))
247 + .c_str());
248 + }
249 + }
250 + }
251 +
252 + // Add any subcommands of this command for validation.
253 + for (auto& sub : current->GetCommands())
254 + {
255 + commands.push_back(std::move(sub));
256 + }
257 + }
258 + }
259 +};
260 +
261 +} // namespace WSLCCLICommandUnitTests
test/windows/wslc/WSLCCLICredStorageUnitTests.cpp new
+163
@@ -0,0 +1,163 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLICredStorageUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + Unit tests for the FileCredStorage and WinCredStorage credential
12 + storage backends. Tests Store, Get, List, and Erase operations.
13 +
14 +--*/
15 +
16 +#include "precomp.h"
17 +#include "windows/Common.h"
18 +#include "FileCredStorage.h"
19 +#include "WinCredStorage.h"
20 +
21 +using namespace wsl::windows::wslc::services;
22 +using namespace WEX::Logging;
23 +using namespace WEX::Common;
24 +using namespace WEX::TestExecution;
25 +
26 +namespace WSLCCredStorageUnitTests {
27 +
28 +class WSLCCLICredStorageUnitTests
29 +{
30 + WSL_TEST_CLASS(WSLCCLICredStorageUnitTests)
31 +
32 + FileCredStorage m_fileStorage;
33 + WinCredStorage m_winCredStorage;
34 +
35 + static void TestStoreAndGetRoundTrips(ICredentialStorage& storage)
36 + {
37 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { storage.Erase("wslc-test-server1"); });
38 + storage.Store("wslc-test-server1", "test-user", "credential-data-1");
39 +
40 + auto [username, secret] = storage.Get("wslc-test-server1");
41 + VERIFY_ARE_EQUAL(std::string("test-user"), username);
42 + VERIFY_ARE_EQUAL(std::string("credential-data-1"), secret);
43 + }
44 +
45 + WSLC_TEST_METHOD(FileCred_Store_And_Get_RoundTrips)
46 + {
47 + TestStoreAndGetRoundTrips(m_fileStorage);
48 + }
49 + WSLC_TEST_METHOD(WinCred_Store_And_Get_RoundTrips)
50 + {
51 + TestStoreAndGetRoundTrips(m_winCredStorage);
52 + }
53 +
54 + static void TestGetNonExistentReturnsEmpty(ICredentialStorage& storage)
55 + {
56 + auto [username, secret] = storage.Get("wslc-test-nonexistent-server");
57 + VERIFY_IS_TRUE(username.empty());
58 + VERIFY_IS_TRUE(secret.empty());
59 + }
60 +
61 + WSLC_TEST_METHOD(FileCred_Get_NonExistent_ReturnsEmpty)
62 + {
63 + TestGetNonExistentReturnsEmpty(m_fileStorage);
64 + }
65 + WSLC_TEST_METHOD(WinCred_Get_NonExistent_ReturnsEmpty)
66 + {
67 + TestGetNonExistentReturnsEmpty(m_winCredStorage);
68 + }
69 +
70 + static void TestStoreOverwritesExistingCredential(ICredentialStorage& storage)
71 + {
72 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { storage.Erase("wslc-test-server2"); });
73 + storage.Store("wslc-test-server2", "old-user", "old-credential");
74 + storage.Store("wslc-test-server2", "new-user", "new-credential");
75 +
76 + auto [username, secret] = storage.Get("wslc-test-server2");
77 + VERIFY_ARE_EQUAL(std::string("new-user"), username);
78 + VERIFY_ARE_EQUAL(std::string("new-credential"), secret);
79 + }
80 +
81 + WSLC_TEST_METHOD(FileCred_Store_Overwrites_ExistingCredential)
82 + {
83 + TestStoreOverwritesExistingCredential(m_fileStorage);
84 + }
85 + WSLC_TEST_METHOD(WinCred_Store_Overwrites_ExistingCredential)
86 + {
87 + TestStoreOverwritesExistingCredential(m_winCredStorage);
88 + }
89 +
90 + static void TestListContainsStoredServers(ICredentialStorage& storage)
91 + {
92 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
93 + storage.Erase("wslc-test-list1");
94 + storage.Erase("wslc-test-list2");
95 + });
96 + storage.Store("wslc-test-list1", "user1", "cred1");
97 + storage.Store("wslc-test-list2", "user2", "cred2");
98 +
99 + auto servers = storage.List();
100 + bool found1 = false, found2 = false;
101 + for (const auto& s : servers)
102 + {
103 + if (s == L"wslc-test-list1")
104 + {
105 + found1 = true;
106 + }
107 + if (s == L"wslc-test-list2")
108 + {
109 + found2 = true;
110 + }
111 + }
112 +
113 + VERIFY_IS_TRUE(found1);
114 + VERIFY_IS_TRUE(found2);
115 + }
116 +
117 + WSLC_TEST_METHOD(FileCred_List_ContainsStoredServers)
118 + {
119 + TestListContainsStoredServers(m_fileStorage);
120 + }
121 + WSLC_TEST_METHOD(WinCred_List_ContainsStoredServers)
122 + {
123 + TestListContainsStoredServers(m_winCredStorage);
124 + }
125 +
126 + static void TestEraseRemovesCredential(ICredentialStorage& storage)
127 + {
128 + storage.Store("wslc-test-erase", "user", "cred");
129 + auto [username, secret] = storage.Get("wslc-test-erase");
130 + VERIFY_IS_FALSE(username.empty());
131 +
132 + storage.Erase("wslc-test-erase");
133 + auto [username2, secret2] = storage.Get("wslc-test-erase");
134 + VERIFY_IS_TRUE(username2.empty());
135 + }
136 +
137 + WSLC_TEST_METHOD(FileCred_Erase_RemovesCredential)
138 + {
139 + TestEraseRemovesCredential(m_fileStorage);
140 + }
141 + WSLC_TEST_METHOD(WinCred_Erase_RemovesCredential)
142 + {
143 + TestEraseRemovesCredential(m_winCredStorage);
144 + }
145 +
146 + static void TestEraseNonExistentThrows(ICredentialStorage& storage)
147 + {
148 + VERIFY_THROWS_SPECIFIC(storage.Erase("wslc-test-nonexistent-erase"), wil::ResultException, [](const wil::ResultException& e) {
149 + return e.GetErrorCode() == E_NOT_SET;
150 + });
151 + }
152 +
153 + WSLC_TEST_METHOD(FileCred_Erase_NonExistent_Throws)
154 + {
155 + TestEraseNonExistentThrows(m_fileStorage);
156 + }
157 + WSLC_TEST_METHOD(WinCred_Erase_NonExistent_Throws)
158 + {
159 + TestEraseNonExistentThrows(m_winCredStorage);
160 + }
161 +};
162 +
163 +} // namespace WSLCCredStorageUnitTests
test/windows/wslc/WSLCCLIEnvVarParserUnitTests.cpp new
+219
@@ -0,0 +1,219 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLIEnvVarParserUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC CLI environment variable parsing and validation.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "WSLCCLITestHelpers.h"
18 +#include "ContainerModel.h"
19 +
20 +#include <filesystem>
21 +#include <fstream>
22 +
23 +using namespace wsl::windows::wslc;
24 +
25 +namespace WSLCCLIEnvVarParserUnitTests {
26 +
27 +class WSLCCLIEnvVarParserUnitTests
28 +{
29 + WSLC_TEST_CLASS(WSLCCLIEnvVarParserUnitTests)
30 +
31 + TEST_METHOD_SETUP(TestMethodSetup)
32 + {
33 + EnvTestFile = wsl::windows::common::filesystem::GetTempFilename();
34 + return true;
35 + }
36 +
37 + TEST_METHOD_CLEANUP(TestMethodCleanup)
38 + {
39 + DeleteFileW(EnvTestFile.c_str());
40 + return true;
41 + }
42 +
43 + TEST_METHOD(WSLCCLIEnvVarParser_ValidEnvVars)
44 + {
45 + const auto parsed = models::EnvironmentVariable::Parse(L"FOO=bar");
46 + VERIFY_IS_TRUE(parsed.has_value());
47 + VERIFY_ARE_EQUAL(L"FOO=bar", parsed.value());
48 + }
49 +
50 + TEST_METHOD(WSLCCLIEnvVarParser_UsesProcessEnvWhenValueMissing)
51 + {
52 + constexpr const auto key = L"WSLC_TEST_ENV_FROM_PROCESS";
53 + VERIFY_IS_TRUE(SetEnvironmentVariableW(key, L"process_value"));
54 +
55 + auto cleanup = wil::scope_exit([&] { SetEnvironmentVariableW(key, nullptr); });
56 +
57 + const auto parsed = models::EnvironmentVariable::Parse(key);
58 + VERIFY_IS_TRUE(parsed.has_value());
59 + VERIFY_ARE_EQUAL(L"WSLC_TEST_ENV_FROM_PROCESS=process_value", parsed.value());
60 + }
61 +
62 + TEST_METHOD(WSLCCLIEnvVarParser_NulloptForWhitespaceOrUnsetVar)
63 + {
64 + const auto whitespaceOnly = models::EnvironmentVariable::Parse(L" \t ");
65 + VERIFY_IS_FALSE(whitespaceOnly.has_value());
66 +
67 + SetEnvironmentVariableA("WSLC_TEST_ENV_UNSET", nullptr);
68 + const auto missingFromProcess = models::EnvironmentVariable::Parse(L"WSLC_TEST_ENV_UNSET");
69 + VERIFY_IS_FALSE(missingFromProcess.has_value());
70 + }
71 +
72 + TEST_METHOD(WSLCCLIEnvVarParser_InvalidKeysThrow)
73 + {
74 + auto verifyThrowsWithMessage = [](const std::wstring& input, const std::wstring& expectedSubstring) {
75 + try
76 + {
77 + (void)models::EnvironmentVariable::Parse(input);
78 + VERIFY_FAIL(L"Expected exception");
79 + }
80 + catch (const wil::ResultException& ex)
81 + {
82 + VERIFY_ARE_EQUAL(E_INVALIDARG, ex.GetErrorCode());
83 +
84 + const auto raw = ex.GetFailureInfo().pszMessage;
85 + std::wstring message = raw ? raw : L"";
86 + VERIFY_ARE_EQUAL(expectedSubstring, message);
87 + }
88 + };
89 +
90 + verifyThrowsWithMessage(L"=value", L"Environment variable key cannot be empty");
91 + verifyThrowsWithMessage(L"BAD KEY=value", L"Environment variable key 'BAD KEY' cannot contain whitespace");
92 + verifyThrowsWithMessage(L"BAD\tKEY=value", L"Environment variable key 'BAD\tKEY' cannot contain whitespace");
93 + verifyThrowsWithMessage(L"BAD\nKEY=value", L"Environment variable key 'BAD\nKEY' cannot contain whitespace");
94 + }
95 +
96 + TEST_METHOD(WSLCCLIEnvVarParser_ParseFileParsesAndSkipsExpectedLines)
97 + {
98 + constexpr const auto key = L"WSLC_TEST_ENV_FROM_FILE";
99 + VERIFY_IS_TRUE(SetEnvironmentVariableW(key, L"file_process_value") == TRUE);
100 +
101 + auto envCleanup = wil::scope_exit([&] { SetEnvironmentVariableW(key, nullptr); });
102 +
103 + std::ofstream file(EnvTestFile);
104 + VERIFY_IS_TRUE(file.is_open());
105 + file << "# comment\n";
106 + file << "\n";
107 + file << "KEY1=VALUE1\n";
108 + file << " KEY2=VALUE2\n";
109 + file << "WSLC_TEST_ENV_FROM_FILE\n";
110 + file << "WSLC_TEST_ENV_DOES_NOT_EXIST\n";
111 + file.close();
112 +
113 + const auto parsed = models::EnvironmentVariable::ParseFile(EnvTestFile.wstring());
114 +
115 + VERIFY_ARE_EQUAL(3U, parsed.size());
116 + VERIFY_ARE_EQUAL(L"KEY1=VALUE1", parsed[0]);
117 + VERIFY_ARE_EQUAL(L"KEY2=VALUE2", parsed[1]);
118 + VERIFY_ARE_EQUAL(L"WSLC_TEST_ENV_FROM_FILE=file_process_value", parsed[2]);
119 + }
120 +
121 + TEST_METHOD(WSLCCLIEnvVarParser_ParseFileThrowsWhenMissing)
122 + {
123 + try
124 + {
125 + (void)models::EnvironmentVariable::ParseFile(L"ENV_FILE_NOT_FOUND");
126 + VERIFY_FAIL(L"Expected exception");
127 + }
128 + catch (const wil::ResultException& ex)
129 + {
130 + VERIFY_ARE_EQUAL(E_INVALIDARG, ex.GetErrorCode());
131 +
132 + const auto raw = ex.GetFailureInfo().pszMessage;
133 + std::wstring message = raw ? raw : L"";
134 + VERIFY_ARE_EQUAL(L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading", message);
135 + }
136 + }
137 +
138 + TEST_METHOD(WSLCCLIEnvVarParser_ExplicitEmptyValueIsValid)
139 + {
140 + const auto parsed = models::EnvironmentVariable::Parse(L"FOO=");
141 + VERIFY_IS_TRUE(parsed.has_value());
142 + VERIFY_ARE_EQUAL(L"FOO=", parsed.value());
143 + }
144 +
145 + TEST_METHOD(WSLCCLIEnvVarParser_MultipleEqualsPreservedInValue)
146 + {
147 + const auto parsed = models::EnvironmentVariable::Parse(L"FOO=a=b=c");
148 + VERIFY_IS_TRUE(parsed.has_value());
149 + VERIFY_ARE_EQUAL(L"FOO=a=b=c", parsed.value());
150 + }
151 +
152 + TEST_METHOD(WSLCCLIEnvVarParser_EmptyInputReturnsNullopt)
153 + {
154 + const auto parsed = models::EnvironmentVariable::Parse(L"");
155 + VERIFY_IS_FALSE(parsed.has_value());
156 + }
157 +
158 + TEST_METHOD(WSLCCLIEnvVarParser_UsesProcessEnvWhenValueIsExplicitlyEmpty)
159 + {
160 + constexpr const auto key = L"WSLC_TEST_ENV_EMPTY_VALUE";
161 + VERIFY_IS_TRUE(SetEnvironmentVariableW(key, L""));
162 +
163 + auto cleanup = wil::scope_exit([&] { SetEnvironmentVariableW(key, nullptr); });
164 +
165 + const auto parsed = models::EnvironmentVariable::Parse(key);
166 + VERIFY_IS_TRUE(parsed.has_value());
167 + VERIFY_ARE_EQUAL(L"WSLC_TEST_ENV_EMPTY_VALUE=", parsed.value());
168 + }
169 +
170 + TEST_METHOD(WSLCCLIEnvVarParser_ParseFilePreservesTrailingWhitespaceInValue)
171 + {
172 + std::ofstream file(EnvTestFile);
173 + VERIFY_IS_TRUE(file.is_open());
174 + file << "KEY=value \n";
175 + file.close();
176 +
177 + const auto parsed = models::EnvironmentVariable::ParseFile(EnvTestFile.wstring());
178 +
179 + VERIFY_ARE_EQUAL(1U, parsed.size());
180 + VERIFY_ARE_EQUAL(L"KEY=value ", parsed[0]);
181 + }
182 +
183 + TEST_METHOD(WSLCCLIEnvVarParser_ParseFileThrowsOnInvalidLine)
184 + {
185 + try
186 + {
187 + std::ofstream file(EnvTestFile);
188 + VERIFY_IS_TRUE(file.is_open());
189 + file << "BAD KEY=value\n";
190 + file.close();
191 +
192 + (void)models::EnvironmentVariable::ParseFile(EnvTestFile.wstring());
193 + VERIFY_FAIL(L"Expected exception");
194 + }
195 + catch (const wil::ResultException& ex)
196 + {
197 + VERIFY_ARE_EQUAL(E_INVALIDARG, ex.GetErrorCode());
198 +
199 + const auto raw = ex.GetFailureInfo().pszMessage;
200 + std::wstring message = raw ? raw : L"";
201 + VERIFY_ARE_EQUAL(L"Environment variable key 'BAD KEY' cannot contain whitespace", message);
202 + }
203 + }
204 +
205 + TEST_METHOD(WSLCCLIEnvVarParser_ParseFileEmptyFileReturnsEmpty)
206 + {
207 + std::ofstream file(EnvTestFile);
208 + VERIFY_IS_TRUE(file.is_open());
209 + file.close();
210 +
211 + const auto parsed = models::EnvironmentVariable::ParseFile(EnvTestFile.wstring());
212 + VERIFY_ARE_EQUAL(0U, parsed.size());
213 + }
214 +
215 +private:
216 + std::filesystem::path EnvTestFile;
217 +};
218 +
219 +} // namespace WSLCCLIEnvVarParserUnitTests
\ No newline at end of file
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp new
+356
@@ -0,0 +1,356 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLIExecutionUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC CLI command execution.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "WSLCCLITestHelpers.h"
18 +
19 +#include "SessionModel.h"
20 +
21 +#include "Command.h"
22 +#include "RootCommand.h"
23 +#include "ContainerCommand.h"
24 +#include "ContainerTasks.h"
25 +
26 +using namespace wsl::windows::wslc;
27 +using namespace WSLCTestHelpers;
28 +using namespace WEX::Logging;
29 +using namespace WEX::Common;
30 +using namespace WEX::TestExecution;
31 +
32 +namespace WSLCCLIExecutionUnitTests {
33 +// Helper structure to hold test data
34 +struct CommandLineTestCase
35 +{
36 + std::wstring commandLine;
37 + std::wstring expectedCommand;
38 + bool shouldSucceed;
39 +};
40 +
41 +class WSLCCLIExecutionUnitTests
42 +{
43 + WSLC_TEST_CLASS(WSLCCLIExecutionUnitTests)
44 +
45 + TEST_CLASS_SETUP(TestClassSetup)
46 + {
47 + return true;
48 + }
49 +
50 + TEST_CLASS_CLEANUP(TestClassCleanup)
51 + {
52 + return true;
53 + }
54 +
55 + // Test: Verify EnumVariantMap on DataMap for Context Data
56 + TEST_METHOD(EnumVariantMap_DataMapValidation)
57 + {
58 + // DataMap is an EnumVariantMap, but for command execution context data instead of arguments.
59 + // It does not have rigid typing like the Args map, so this will verify every Data enum value
60 + // can be added and retrieved successfully. The arguments unit tests have more complex tests
61 + // for the EnumVariantMap behavior. This one ensures Data enum values are correct.
62 + wsl::windows::wslc::execution::DataMap dataMap;
63 +
64 + // Verify all data enum values defined.
65 + auto allDataTypes = std::vector<Data>{};
66 + for (int i = 0; i < static_cast<int>(Data::Max); ++i)
67 + {
68 + Data dataType = static_cast<Data>(i);
69 +
70 + // Add the data to the DataMap with a test value based on its type.
71 + // Each data type needs to be added here as each enum may have its own value.
72 + VERIFY_IS_FALSE(dataMap.Contains(dataType));
73 + bool handled = false;
74 + if (dataType == Data::Session)
75 + {
76 + // Create a null session for testing - Session requires a COM pointer
77 + wil::com_ptr<IWSLCSession> nullSession; // Creates null COM pointer
78 + wsl::windows::wslc::models::Session session{nullSession};
79 + dataMap.Add<Data::Session>(std::move(session));
80 + handled = true;
81 + }
82 + else if (dataType == Data::Containers)
83 + {
84 + std::vector<wsl::windows::wslc::models::ContainerInformation> containers;
85 + dataMap.Add<Data::Containers>(std::move(containers));
86 + handled = true;
87 + }
88 + else if (dataType == Data::ContainerOptions)
89 + {
90 + wsl::windows::wslc::models::ContainerOptions options;
91 + dataMap.Add<Data::ContainerOptions>(std::move(options));
92 + handled = true;
93 + }
94 + else if (dataType == Data::Images)
95 + {
96 + std::vector<wsl::windows::wslc::models::ImageInformation> images;
97 + dataMap.Add<Data::Images>(std::move(images));
98 + handled = true;
99 + }
100 + else if (dataType == Data::Volumes)
101 + {
102 + std::vector<WSLCVolumeInformation> volumes;
103 + dataMap.Add<Data::Volumes>(std::move(volumes));
104 + handled = true;
105 + }
106 +
107 + if (!handled)
108 + {
109 + VERIFY_FAIL(L"Unhandled Data type in test");
110 + }
111 +
112 + allDataTypes.push_back(dataType);
113 + VERIFY_IS_TRUE(dataMap.Contains(dataType));
114 + }
115 +
116 + // Verify basic retrieval.
117 + auto& session = dataMap.Get<Data::Session>();
118 + VERIFY_IS_NULL(session.Get()); // A null ptr was added.
119 +
120 + auto& containers = dataMap.Get<Data::Containers>();
121 + VERIFY_ARE_EQUAL(0u, containers.size());
122 +
123 + // Other more complex EnumVariantMap tests are in the Args unit tests.
124 + // This one will just verify all the data types in the Data Map work as expected.
125 + }
126 +
127 + // Test: SetContainerOptionsFromArgs sets WorkingDirectory when --workdir is provided
128 + TEST_METHOD(SetContainerOptionsFromArgs_WithWorkDir_SetsWorkingDirectory)
129 + {
130 + CLIExecutionContext context;
131 + context.Args.Add<ArgType::WorkDir>(std::wstring{L"/app"});
132 +
133 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
134 +
135 + const auto& options = context.Data.Get<Data::ContainerOptions>();
136 + VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
137 + }
138 +
139 + // Test: SetContainerOptionsFromArgs leaves WorkingDirectory empty when --workdir is not provided
140 + TEST_METHOD(SetContainerOptionsFromArgs_WithoutWorkDir_WorkingDirectoryIsEmpty)
141 + {
142 + CLIExecutionContext context;
143 +
144 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
145 +
146 + const auto& options = context.Data.Get<Data::ContainerOptions>();
147 + VERIFY_IS_TRUE(options.WorkingDirectory.empty());
148 + }
149 +
150 + // Test: Full parse of 'exec --workdir "" cont1 cmd' rejects empty working directory
151 + TEST_METHOD(ExecCommand_ParseWorkDirEmptyValue_ThrowsArgumentException)
152 + {
153 + // Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts.
154 + auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" cont1 sh");
155 +
156 + ContainerExecCommand command{L""};
157 + CLIExecutionContext context;
158 + command.ParseArguments(invocation, context.Args);
159 +
160 + VERIFY_THROWS_SPECIFIC(
161 + command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
162 + }
163 +
164 + // Test: Full parse of 'exec --workdir /path cont1 cmd' sets WorkingDirectory
165 + TEST_METHOD(ExecCommand_ParseWorkDirLongOption_SetsWorkingDirectory)
166 + {
167 + // Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts.
168 + auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir cont1 sh");
169 +
170 + ContainerExecCommand command{L""};
171 + CLIExecutionContext context;
172 + command.ParseArguments(invocation, context.Args);
173 + command.ValidateArguments(context.Args);
174 +
175 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
176 +
177 + const auto& options = context.Data.Get<Data::ContainerOptions>();
178 + VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory);
179 + }
180 +
181 + // Test: Full parse of 'exec -w /path cont1 cmd' (short alias) sets WorkingDirectory
182 + TEST_METHOD(ExecCommand_ParseWorkDirShortOption_SetsWorkingDirectory)
183 + {
184 + auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app cont1 sh");
185 +
186 + ContainerExecCommand command{L""};
187 + CLIExecutionContext context;
188 + command.ParseArguments(invocation, context.Args);
189 + command.ValidateArguments(context.Args);
190 +
191 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
192 +
193 + const auto& options = context.Data.Get<Data::ContainerOptions>();
194 + VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
195 + }
196 +
197 + // Test: Full parse of 'run --workdir "" image cmd' rejects empty working directory
198 + TEST_METHOD(RunCommand_ParseWorkDirEmptyValue_ThrowsArgumentException)
199 + {
200 + auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" ubuntu sh");
201 +
202 + ContainerRunCommand command{L""};
203 + CLIExecutionContext context;
204 + command.ParseArguments(invocation, context.Args);
205 +
206 + VERIFY_THROWS_SPECIFIC(
207 + command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
208 + }
209 +
210 + // Test: Full parse of 'run --workdir /path image cmd' sets WorkingDirectory
211 + TEST_METHOD(RunCommand_ParseWorkDirLongOption_SetsWorkingDirectory)
212 + {
213 + auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir ubuntu sh");
214 +
215 + ContainerRunCommand command{L""};
216 + CLIExecutionContext context;
217 + command.ParseArguments(invocation, context.Args);
218 + command.ValidateArguments(context.Args);
219 +
220 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
221 +
222 + const auto& options = context.Data.Get<Data::ContainerOptions>();
223 + VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory);
224 + }
225 +
226 + // Test: Full parse of 'run -w /path image cmd' (short alias) sets WorkingDirectory
227 + TEST_METHOD(RunCommand_ParseWorkDirShortOption_SetsWorkingDirectory)
228 + {
229 + auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app ubuntu sh");
230 +
231 + ContainerRunCommand command{L""};
232 + CLIExecutionContext context;
233 + command.ParseArguments(invocation, context.Args);
234 + command.ValidateArguments(context.Args);
235 +
236 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
237 +
238 + const auto& options = context.Data.Get<Data::ContainerOptions>();
239 + VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
240 + }
241 +
242 + // Test: Full parse of 'create --workdir "" image cmd' rejects empty working directory
243 + TEST_METHOD(CreateCommand_ParseWorkDirEmptyValue_ThrowsArgumentException)
244 + {
245 + auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" ubuntu sh");
246 +
247 + ContainerCreateCommand command{L""};
248 + CLIExecutionContext context;
249 + command.ParseArguments(invocation, context.Args);
250 +
251 + VERIFY_THROWS_SPECIFIC(
252 + command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
253 + }
254 +
255 + // Test: Full parse of 'create --workdir /path image cmd' sets WorkingDirectory
256 + TEST_METHOD(CreateCommand_ParseWorkDirLongOption_SetsWorkingDirectory)
257 + {
258 + auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir ubuntu sh");
259 +
260 + ContainerCreateCommand command{L""};
261 + CLIExecutionContext context;
262 + command.ParseArguments(invocation, context.Args);
263 + command.ValidateArguments(context.Args);
264 +
265 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
266 +
267 + const auto& options = context.Data.Get<Data::ContainerOptions>();
268 + VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory);
269 + }
270 +
271 + // Test: Full parse of 'create -w /path image cmd' (short alias) sets WorkingDirectory
272 + TEST_METHOD(CreateCommand_ParseWorkDirShortOption_SetsWorkingDirectory)
273 + {
274 + auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app ubuntu sh");
275 +
276 + ContainerCreateCommand command{L""};
277 + CLIExecutionContext context;
278 + command.ParseArguments(invocation, context.Args);
279 + command.ValidateArguments(context.Args);
280 +
281 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
282 +
283 + const auto& options = context.Data.Get<Data::ContainerOptions>();
284 + VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
285 + }
286 +
287 + // Test: Command Line test parsing all cases defined in CommandLineTestCases.h
288 + // This test verifies the command line parsing logic used by the CLI and executes the same
289 + // code as the CLI up to the point of command execution, including parsing and argument validtion.
290 + // It does not actually verify the execution of the command, just that the correct command is
291 + // found and the provided command line parsed correctly according to the command's defined arguments,
292 + // and the argument validation rules are correctly applied. The test cases are defined in
293 + // CommandLineTestCases.h and cover various valid and invalid command lines.
294 + TEST_METHOD(CommandLineParsing_AllCases)
295 + {
296 + std::vector<CommandLineTestCase> testCases = {
297 +#define COMMAND_LINE_TEST_CASE(cmdLine, expectedCmd, shouldPass) {cmdLine, expectedCmd, shouldPass},
298 +#include "CommandLineTestCases.h"
299 +#undef COMMAND_LINE_TEST_CASE
300 + };
301 +
302 + // Run all test cases
303 + for (const auto& testCase : testCases)
304 + {
305 + LogComment(L"Testing: " + testCase.commandLine);
306 +
307 + // Pre-pend executable name, which will get stripped off by CommandLineToArgvW
308 + auto fullCommandLine = L"wslc " + testCase.commandLine;
309 +
310 + // Process the command line as Windows does.
311 + int argc = 0;
312 + auto argv = CommandLineToArgvW(fullCommandLine.c_str(), &argc);
313 + std::vector<std::wstring> args;
314 + for (int i = 1; i < argc; ++i)
315 + {
316 + args.emplace_back(argv[i]);
317 + }
318 +
319 + // And now process the command line like WSLC does.
320 + bool succeeded = true;
321 + try
322 + {
323 + Invocation invocation{std::move(args)};
324 + std::unique_ptr<Command> command = std::make_unique<RootCommand>();
325 + std::unique_ptr<Command> subCommand = command->FindSubCommand(invocation);
326 + while (subCommand)
327 + {
328 + command = std::move(subCommand);
329 + subCommand = command->FindSubCommand(invocation);
330 + }
331 +
332 + // Ensure we found the expected command
333 + VERIFY_ARE_EQUAL(testCase.expectedCommand, command->Name());
334 +
335 + CLIExecutionContext context;
336 +
337 + // Parse and validate and compare to expected results.
338 + command->ParseArguments(invocation, context.Args);
339 + command->ValidateArguments(context.Args);
340 + }
341 + catch (const CommandException& ce)
342 + {
343 + LogComment(L"Command line parsing threw an exception: " + ce.Message());
344 + succeeded = false;
345 + }
346 + catch (...)
347 + {
348 + LogComment(L"Command line parsing threw an unexpected exception.");
349 + succeeded = false;
350 + }
351 +
352 + VERIFY_ARE_EQUAL(testCase.shouldSucceed, succeeded);
353 + }
354 + }
355 +};
356 +} // namespace WSLCCLIExecutionUnitTests
\ No newline at end of file
test/windows/wslc/WSLCCLILabelParserUnitTests.cpp new
+71
@@ -0,0 +1,71 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLILabelParserUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC CLI label parsing and validation.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCCLITestHelpers.h"
17 +#include "VolumeModel.h"
18 +
19 +using namespace wsl::windows::wslc;
20 +
21 +namespace WSLCCLILabelParserUnitTests {
22 +
23 +class WSLCCLILabelParserUnitTests
24 +{
25 + WSLC_TEST_CLASS(WSLCCLILabelParserUnitTests)
26 +
27 + TEST_METHOD(WSLCCLILabelParser_ValidLabels)
28 + {
29 + std::vector<std::tuple<std::wstring, std::string, std::string>> validLabels = {
30 + {L"foo=bar", "foo", "bar"},
31 + {L"foo=", "foo", ""},
32 + {L"foo", "foo", ""},
33 + {L"foo=a=b=c", "foo", "a=b=c"},
34 + };
35 +
36 + for (const auto& [input, expectedKey, expectedValue] : validLabels)
37 + {
38 + auto result = models::Label::Parse(input);
39 + VERIFY_ARE_EQUAL(expectedKey, result.first);
40 + VERIFY_ARE_EQUAL(expectedValue, result.second);
41 + }
42 + }
43 +
44 + TEST_METHOD(WSLCCLILabelParser_InvalidLabels)
45 + {
46 + std::vector<std::wstring> invalidLabels = {
47 + L"",
48 + L"=",
49 + L"=value",
50 + };
51 +
52 + for (const auto& input : invalidLabels)
53 + {
54 + try
55 + {
56 + (void)models::Label::Parse(input);
57 + VERIFY_FAIL(L"Expected exception");
58 + }
59 + catch (const wil::ResultException& ex)
60 + {
61 + VERIFY_ARE_EQUAL(E_INVALIDARG, ex.GetErrorCode());
62 +
63 + const auto raw = ex.GetFailureInfo().pszMessage;
64 + std::wstring message = raw ? raw : L"";
65 + VERIFY_ARE_EQUAL(L"Label key cannot be empty", message);
66 + }
67 + }
68 + }
69 +};
70 +
71 +} // namespace WSLCCLILabelParserUnitTests
test/windows/wslc/WSLCCLIOptionsParserUnitTests.cpp new
+47
@@ -0,0 +1,47 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLIOptionsParserUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC CLI driver option parsing.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCCLITestHelpers.h"
17 +#include "VolumeModel.h"
18 +
19 +using namespace wsl::windows::wslc;
20 +
21 +namespace WSLCCLIOptionsParserUnitTests {
22 +
23 +class WSLCCLIOptionsParserUnitTests
24 +{
25 + WSLC_TEST_CLASS(WSLCCLIOptionsParserUnitTests)
26 +
27 + TEST_METHOD(WSLCCLIOptionsParser_ValidOptions)
28 + {
29 + std::vector<std::tuple<std::wstring, std::string, std::string>> validOptions = {
30 + {L"SizeBytes=3145728", "SizeBytes", "3145728"},
31 + {L"ReadOnly", "ReadOnly", ""},
32 + {L"key=", "key", ""},
33 + {L"=value", "", "value"},
34 + {L"", "", ""},
35 + {L"key=a=b=c", "key", "a=b=c"},
36 + };
37 +
38 + for (const auto& [input, expectedKey, expectedValue] : validOptions)
39 + {
40 + auto result = models::DriverOption::Parse(input);
41 + VERIFY_ARE_EQUAL(expectedKey, result.first);
42 + VERIFY_ARE_EQUAL(expectedValue, result.second);
43 + }
44 + }
45 +};
46 +
47 +} // namespace WSLCCLIOptionsParserUnitTests
test/windows/wslc/WSLCCLIParserUnitTests.cpp new
+176
@@ -0,0 +1,176 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLIParserUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC CLI argument parsing and validation.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "WSLCCLITestHelpers.h"
18 +
19 +#include "Argument.h"
20 +#include "ArgumentTypes.h"
21 +#include "ArgumentParser.h"
22 +#include "Invocation.h"
23 +#include "ParserTestCases.h"
24 +
25 +using namespace wsl::windows::wslc;
26 +using namespace wsl::windows::wslc::argument;
27 +
28 +using namespace WSLCTestHelpers;
29 +using namespace WEX::Logging;
30 +using namespace WEX::Common;
31 +using namespace WEX::TestExecution;
32 +
33 +namespace WSLCCLIParserUnitTests {
34 +
35 +class WSLCCLIParserUnitTests
36 +{
37 + WSLC_TEST_CLASS(WSLCCLIParserUnitTests)
38 +
39 + TEST_CLASS_SETUP(TestClassSetup)
40 + {
41 + return true;
42 + }
43 +
44 + TEST_CLASS_CLEANUP(TestClassCleanup)
45 + {
46 + return true;
47 + }
48 +
49 + // Test: Verify command line to argv mapping and GetRemainingRawCommandLineFromIndex
50 + TEST_METHOD(ParserTest_StateMachine_PositionalForward)
51 + {
52 + // Build test cases from x-macro
53 + std::vector<ParserTestCase> testCases = {
54 +#define WSLC_PARSER_TEST_CASE(argSetValue, expected, cmdLine) {ArgumentSet::argSetValue, expected, cmdLine},
55 + WSLC_PARSER_TEST_CASES
56 +#undef WSLC_PARSER_TEST_CASE
57 + };
58 +
59 + for (const auto& testCase : testCases)
60 + {
61 + bool succeeded = false;
62 +
63 + try
64 + {
65 + Log::Comment(String().Format(L"Testing: %ls", testCase.commandLine.c_str()));
66 + auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(testCase.commandLine);
67 +
68 + // Get argument definitions from the helper function
69 + std::vector<Argument> definedArgs = GetArgumentsForSet(testCase.argumentSet);
70 +
71 + ArgMap args;
72 + ParseArgumentsStateMachine stateMachine{inv, args, std::move(definedArgs)};
73 + while (stateMachine.Step())
74 + {
75 + stateMachine.ThrowIfError();
76 + }
77 +
78 + // Validate count limits and required arguments, mirroring Command::ValidateArguments.
79 + // Skip all validation if --help is present, as Command::ValidateArguments does.
80 + if (!args.Contains(ArgType::Help))
81 + {
82 + for (const auto& arg : GetArgumentsForSet(testCase.argumentSet))
83 + {
84 + if (arg.Required() && !args.Contains(arg.Type()))
85 + {
86 + throw ArgumentException(std::wstring(L"Required argument missing: ") + arg.Name());
87 + }
88 +
89 + if ((arg.Limit() > 0) && (arg.Limit() < args.Count(arg.Type())))
90 + {
91 + throw ArgumentException(std::wstring(L"Too many values for argument: ") + arg.Name());
92 + }
93 +
94 + if (args.Contains(arg.Type()))
95 + {
96 + arg.Validate(args);
97 + }
98 + }
99 + }
100 +
101 + succeeded = true;
102 +
103 + if (testCase.commandLine.find(L"image1") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
104 + {
105 + VERIFY_IS_TRUE(args.Contains(ArgType::ImageId));
106 + auto imageId = args.Get<ArgType::ImageId>();
107 + VERIFY_ARE_EQUAL(L"image1", imageId);
108 + }
109 +
110 + if (testCase.commandLine.find(L"cont1") != std::wstring::npos && testCase.argumentSet == ArgumentSet::List)
111 + {
112 + VERIFY_IS_TRUE(args.Contains(ArgType::ContainerId));
113 + auto containerId = args.Get<ArgType::ContainerId>();
114 + VERIFY_ARE_EQUAL(L"cont1", containerId);
115 + }
116 +
117 + if (testCase.commandLine.find(L"--rm") != std::wstring::npos)
118 + {
119 + // Ensure '--rm' was parsed wherever it was found.
120 + VERIFY_IS_TRUE(args.Contains(ArgType::Remove));
121 + }
122 +
123 + if (testCase.commandLine.find(L"command") != std::wstring::npos)
124 + {
125 + VERIFY_IS_TRUE(args.Contains(ArgType::Command));
126 + auto command = args.Get<ArgType::Command>();
127 + VERIFY_IS_TRUE(command.find(L"command") != std::wstring::npos);
128 + }
129 +
130 + if (testCase.commandLine.find(L"forward") != std::wstring::npos)
131 + {
132 + VERIFY_IS_TRUE(args.Contains(ArgType::ForwardArgs));
133 + auto forwardArgs = args.Get<ArgType::ForwardArgs>();
134 + std::wstring forwardArgsConcat = wsl::shared::string::Join(forwardArgs, L' ');
135 + VERIFY_IS_TRUE(forwardArgsConcat.find(L"hello world") != std::wstring::npos); // Forward args should contain hello world
136 + VERIFY_IS_TRUE(forwardArgsConcat.find(L"image1") == std::wstring::npos); // Forward args should not contain the imageId
137 + VERIFY_IS_TRUE(forwardArgsConcat.find(L"command") == std::wstring::npos); // Forward args should not contain the command
138 + LogComment(L"Forwarded Args: " + forwardArgsConcat);
139 + }
140 +
141 + if (testCase.commandLine.find(L"443") != std::wstring::npos)
142 + {
143 + VERIFY_IS_TRUE(args.Contains(ArgType::Publish));
144 + auto publishArgs = args.GetAll<ArgType::Publish>();
145 + VERIFY_ARE_EQUAL(2, publishArgs.size()); // Should have both publish args
146 + VERIFY_ARE_NOT_EQUAL(publishArgs[0], publishArgs[1]); // Both publish args should be different
147 + }
148 + }
149 + catch (ArgumentException& ex)
150 + {
151 + if (testCase.expectedResult)
152 + {
153 + VERIFY_FAIL(String().Format(L"Test case threw unexpected argument exception: %ls", ex.Message().c_str()));
154 + }
155 + else
156 + {
157 + Log::Comment(String().Format(L"Test case threw expected argument exception: %ls", ex.Message().c_str()));
158 + }
159 + }
160 + catch (std::exception& ex)
161 + {
162 + if (testCase.expectedResult)
163 + {
164 + VERIFY_FAIL(String().Format(L"Test case threw unexpected exception: %hs", ex.what()));
165 + }
166 + else
167 + {
168 + Log::Comment(String().Format(L"Test case threw expected exception: %hs", ex.what()));
169 + }
170 + }
171 +
172 + VERIFY_ARE_EQUAL(testCase.expectedResult, succeeded, String().Format(L"Command line: %ls", testCase.commandLine.c_str()));
173 + }
174 + }
175 +};
176 +} // namespace WSLCCLIParserUnitTests
\ No newline at end of file
test/windows/wslc/WSLCCLISettingsUnitTests.cpp new
+532
@@ -0,0 +1,532 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLISettingsUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + Unit tests for the wslc UserSettings system: SettingsMap, YAML loading,
12 + per-setting validation, fallback logic, and UserSettingsType detection.
13 +
14 +--*/
15 +
16 +#include "precomp.h"
17 +#include "windows/Common.h"
18 +#include "WSLCCLITestHelpers.h"
19 +#include "WSLCUserSettings.h"
20 +
21 +#include <atomic>
22 +#include <fstream>
23 +
24 +using namespace wsl::windows::wslc::settings;
25 +using namespace WSLCTestHelpers;
26 +using namespace WEX::Logging;
27 +using namespace WEX::Common;
28 +using namespace WEX::TestExecution;
29 +using Loc = wsl::shared::Localization;
30 +
31 +namespace WSLCCLISettingsUnitTests {
32 +
33 +// Thin subclass that makes the protected constructor publicly accessible,
34 +// allowing tests to load settings from an arbitrary directory without
35 +// going through the singleton.
36 +class UserSettingsTest : public UserSettings
37 +{
38 +public:
39 + explicit UserSettingsTest(const std::filesystem::path& settingsDir) : UserSettings(settingsDir)
40 + {
41 + }
42 +};
43 +
44 +// ---------------------------------------------------------------------------
45 +// Helpers
46 +// ---------------------------------------------------------------------------
47 +
48 +static std::atomic<int> s_dirCounter{0};
49 +
50 +static std::filesystem::path UniqueTempDir()
51 +{
52 + auto dir = std::filesystem::temp_directory_path() / L"WSLCSettingsTests" / std::to_wstring(GetCurrentProcessId()) /
53 + std::to_wstring(++s_dirCounter);
54 + std::filesystem::create_directories(dir);
55 + return dir;
56 +}
57 +
58 +static void WriteFile(const std::filesystem::path& path, std::string_view content)
59 +{
60 + std::filesystem::create_directories(path.parent_path());
61 + std::ofstream f(path, std::ios::binary);
62 + VERIFY_IS_TRUE(f.is_open());
63 + f.write(content.data(), static_cast<std::streamsize>(content.size()));
64 +}
65 +
66 +// ---------------------------------------------------------------------------
67 +// Test class
68 +// ---------------------------------------------------------------------------
69 +
70 +class WSLCCLISettingsUnitTests
71 +{
72 + WSL_TEST_CLASS(WSLCCLISettingsUnitTests)
73 +
74 + TEST_CLASS_SETUP(TestClassSetup)
75 + {
76 + return true;
77 + }
78 +
79 + TEST_CLASS_CLEANUP(TestClassCleanup)
80 + {
81 + std::error_code ec;
82 + std::filesystem::remove_all(std::filesystem::temp_directory_path() / L"WSLCSettingsTests", ec);
83 + return true;
84 + }
85 +
86 + // -----------------------------------------------------------------------
87 + // SettingsMap — pure unit tests, no I/O
88 + // -----------------------------------------------------------------------
89 +
90 + // All four settings should return their compile-time defaults when the map
91 + // is empty (no values have been inserted).
92 + TEST_METHOD(SettingsMap_GetOrDefault_ReturnsBuiltInWhenAbsent)
93 + {
94 + SettingsMap map;
95 + VERIFY_ARE_EQUAL(0u, map.GetOrDefault<Setting::SessionCpuCount>());
96 + VERIFY_ARE_EQUAL(0u, map.GetOrDefault<Setting::SessionMemoryMb>());
97 + VERIFY_ARE_EQUAL(1048576u, map.GetOrDefault<Setting::SessionStorageSizeMb>());
98 + VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(map.GetOrDefault<Setting::CredentialStore>()));
99 + }
100 +
101 + // After inserting a value, GetOrDefault must return it rather than the default.
102 + TEST_METHOD(SettingsMap_GetOrDefault_ReturnsStoredWhenPresent)
103 + {
104 + SettingsMap map;
105 + map.Add<Setting::SessionCpuCount>(16u);
106 + VERIFY_ARE_EQUAL(16u, map.GetOrDefault<Setting::SessionCpuCount>());
107 + VERIFY_ARE_EQUAL(0u, map.GetOrDefault<Setting::SessionMemoryMb>());
108 + }
109 +
110 + // -----------------------------------------------------------------------
111 + // Default (setting file missing)
112 + // -----------------------------------------------------------------------
113 +
114 + // When settings file missing, the type must be Default, there
115 + // must be no warnings, and all values must be at their built-in defaults.
116 + TEST_METHOD(LoadSettings_NoFiles_YieldsDefaultTypeAndNoWarnings)
117 + {
118 + UserSettingsTest s{UniqueTempDir()};
119 +
120 + VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Default), static_cast<int>(s.GetType()));
121 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
122 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
123 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
124 + VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
125 + VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
126 + }
127 +
128 + // -----------------------------------------------------------------------
129 + // Standard (valid settings)
130 + // -----------------------------------------------------------------------
131 +
132 + // A well-formed settings file must set the type to Standard with no
133 + // warnings and the specified values loaded.
134 + TEST_METHOD(LoadSettings_ValidSettings_YieldsStandardTypeAndValues)
135 + {
136 + auto dir = UniqueTempDir();
137 + WriteFile(
138 + dir / L"settings.yaml",
139 + "session:\n"
140 + " cpuCount: 8\n"
141 + " memorySize: 4GB\n"
142 + " maxStorageSize: 20000MB\n"
143 + "credentialStore: file\n");
144 +
145 + UserSettingsTest s{dir};
146 +
147 + VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
148 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
149 + VERIFY_ARE_EQUAL(8u, s.Get<Setting::SessionCpuCount>());
150 + VERIFY_ARE_EQUAL(4096u, s.Get<Setting::SessionMemoryMb>());
151 + VERIFY_ARE_EQUAL(20000u, s.Get<Setting::SessionStorageSizeMb>());
152 + VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::File), static_cast<int>(s.Get<Setting::CredentialStore>()));
153 + }
154 +
155 + // An empty settings file is valid YAML (null document) but not a mapping;
156 + // a structure warning is emitted and all settings use defaults.
157 + TEST_METHOD(LoadSettings_EmptySettings_WarnsInvalidStructure)
158 + {
159 + auto dir = UniqueTempDir();
160 + WriteFile(dir / L"settings.yaml", "");
161 +
162 + UserSettingsTest s{dir};
163 +
164 + VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
165 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
166 + VERIFY_ARE_EQUAL(Loc::WSLCUserSettings_Warning_InvalidStructure(s.SettingsFilePath().wstring()), s.GetWarnings().front().Message);
167 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
168 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
169 + VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
170 + }
171 +
172 + // A non-map root (e.g. bare scalar) is valid YAML but invalid structure;
173 + // a warning is emitted and all settings use defaults.
174 + TEST_METHOD(LoadSettings_NonMapRoot_WarnsInvalidStructure)
175 + {
176 + auto dir = UniqueTempDir();
177 + WriteFile(dir / L"settings.yaml", "just a string\n");
178 +
179 + UserSettingsTest s{dir};
180 +
181 + VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
182 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
183 + VERIFY_ARE_EQUAL(Loc::WSLCUserSettings_Warning_InvalidStructure(s.SettingsFilePath().wstring()), s.GetWarnings().front().Message);
184 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
185 + }
186 +
187 + // When the settings file fails to parse, the type is Default and a warning is emitted.
188 + TEST_METHOD(LoadSettings_InvalidSettings_YieldsDefaultTypeWithWarning)
189 + {
190 + auto dir = UniqueTempDir();
191 + WriteFile(dir / L"settings.yaml", "session: [\n"); // broken YAML (unclosed flow seq)
192 +
193 + UserSettingsTest s{dir};
194 +
195 + VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Default), static_cast<int>(s.GetType()));
196 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
197 + // Parse errors include yaml-cpp details, so check prefix including the file path.
198 + VERIFY_IS_TRUE(s.GetWarnings().front().Message.starts_with(
199 + L"Warning: Settings file at " + s.SettingsFilePath().wstring() + L" could not be parsed."));
200 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
201 + }
202 +
203 + // -----------------------------------------------------------------------
204 + // Per-setting validation
205 + // -----------------------------------------------------------------------
206 +
207 + // cpuCount: 0 is rejected by validation; the default (0) is used and a warning emitted.
208 + TEST_METHOD(Validation_CpuCount_Zero_UsesDefaultAndWarns)
209 + {
210 + auto dir = UniqueTempDir();
211 + WriteFile(dir / L"settings.yaml", "session:\n cpuCount: 0\n");
212 +
213 + UserSettingsTest s{dir};
214 +
215 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
216 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
217 + VERIFY_ARE_EQUAL(
218 + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.cpuCount", s.SettingsFilePath().wstring(), 2),
219 + s.GetWarnings().front().Message);
220 + VERIFY_ARE_EQUAL(std::wstring(L"session.cpuCount"), s.GetWarnings().front().SettingPath);
221 + }
222 +
223 + // memorySize: 0 is rejected by validation; the default (0) is used.
224 + TEST_METHOD(Validation_MemoryMb_Zero_UsesDefaultAndWarns)
225 + {
226 + auto dir = UniqueTempDir();
227 + WriteFile(dir / L"settings.yaml", "session:\n memorySize: 0\n");
228 +
229 + UserSettingsTest s{dir};
230 +
231 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
232 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
233 + VERIFY_ARE_EQUAL(
234 + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.memorySize", s.SettingsFilePath().wstring(), 2),
235 + s.GetWarnings().front().Message);
236 + }
237 +
238 + // maxStorageSize: 0 must be rejected; the default is used.
239 + TEST_METHOD(Validation_StorageSizeMb_Zero_UsesDefaultAndWarns)
240 + {
241 + auto dir = UniqueTempDir();
242 + WriteFile(dir / L"settings.yaml", "session:\n maxStorageSize: 0\n");
243 +
244 + UserSettingsTest s{dir};
245 +
246 + VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
247 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
248 + VERIFY_ARE_EQUAL(
249 + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.maxStorageSize", s.SettingsFilePath().wstring(), 2),
250 + s.GetWarnings().front().Message);
251 + }
252 +
253 + // A string where a uint32_t is expected must emit a type warning and fall
254 + // back to the default.
255 + TEST_METHOD(Validation_WrongType_UsesDefaultAndWarns)
256 + {
257 + auto dir = UniqueTempDir();
258 + WriteFile(dir / L"settings.yaml", "session:\n cpuCount: \"not-a-number\"\n");
259 +
260 + UserSettingsTest s{dir};
261 +
262 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
263 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
264 + VERIFY_ARE_EQUAL(
265 + Loc::WSLCUserSettings_Warning_InvalidType(L"session.cpuCount", s.SettingsFilePath().wstring(), 2),
266 + s.GetWarnings().front().Message);
267 + }
268 +
269 + // Absent keys must silently use defaults — no warnings emitted.
270 + TEST_METHOD(Validation_AbsentKeys_NoWarningsAndDefaults)
271 + {
272 + auto dir = UniqueTempDir();
273 + WriteFile(dir / L"settings.yaml", "session:\n");
274 +
275 + UserSettingsTest s{dir};
276 +
277 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
278 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
279 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
280 + VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
281 + }
282 +
283 + // The string "default" for any setting must silently use the built-in
284 + // default, same as if the key were absent.
285 + TEST_METHOD(Validation_DefaultString_UsesBuiltInDefaultsNoWarnings)
286 + {
287 + auto dir = UniqueTempDir();
288 + WriteFile(
289 + dir / L"settings.yaml",
290 + "session:\n"
291 + " cpuCount: default\n"
292 + " memorySize: default\n"
293 + " maxStorageSize: default\n"
294 + " networkingMode: default\n"
295 + " hostFileShareMode: default\n"
296 + " dnsTunneling: default\n"
297 + "credentialStore: default\n");
298 +
299 + UserSettingsTest s{dir};
300 +
301 + VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
302 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
303 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
304 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
305 + VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
306 + VERIFY_ARE_EQUAL(static_cast<int>(WSLCNetworkingModeVirtioProxy), static_cast<int>(s.Get<Setting::SessionNetworkingMode>()));
307 + VERIFY_ARE_EQUAL(static_cast<int>(HostFileShareMode::VirtioFs), static_cast<int>(s.Get<Setting::SessionHostFileShareMode>()));
308 + VERIFY_IS_TRUE(s.Get<Setting::SessionDnsTunneling>());
309 + VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
310 + }
311 +
312 + // "default" on a single setting uses the built-in default for that setting
313 + // while explicit values on other settings are preserved.
314 + TEST_METHOD(Validation_DefaultString_MixedWithExplicitValues)
315 + {
316 + auto dir = UniqueTempDir();
317 + WriteFile(
318 + dir / L"settings.yaml",
319 + "session:\n"
320 + " cpuCount: 8\n"
321 + " memorySize: default\n"
322 + " maxStorageSize: 50000MB\n"
323 + "credentialStore: default\n");
324 +
325 + UserSettingsTest s{dir};
326 +
327 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
328 + VERIFY_ARE_EQUAL(8u, s.Get<Setting::SessionCpuCount>());
329 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
330 + VERIFY_ARE_EQUAL(50000u, s.Get<Setting::SessionStorageSizeMb>());
331 + VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
332 + }
333 +
334 + // Quoted "default" string must behave the same as unquoted default.
335 + TEST_METHOD(Validation_DefaultString_QuotedIsAlsoValid)
336 + {
337 + auto dir = UniqueTempDir();
338 + WriteFile(
339 + dir / L"settings.yaml",
340 + "session:\n"
341 + " cpuCount: \"default\"\n"
342 + " memorySize: \"default\"\n");
343 +
344 + UserSettingsTest s{dir};
345 +
346 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
347 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
348 + VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
349 + }
350 +
351 + // "Default" (capitalized) is NOT the magic string — it must be treated as
352 + // an invalid value and fall back to the built-in default with a warning.
353 + TEST_METHOD(Validation_DefaultString_IsCaseSensitive)
354 + {
355 + auto dir = UniqueTempDir();
356 + WriteFile(
357 + dir / L"settings.yaml",
358 + "session:\n"
359 + " networkingMode: Default\n"
360 + "credentialStore: DEFAULT\n");
361 +
362 + UserSettingsTest s{dir};
363 +
364 + // Both should be rejected by their validators and produce warnings.
365 + VERIFY_ARE_EQUAL(2u, s.GetWarnings().size());
366 + VERIFY_ARE_EQUAL(
367 + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.networkingMode", s.SettingsFilePath().wstring(), 2),
368 + s.GetWarnings()[0].Message);
369 + VERIFY_ARE_EQUAL(
370 + Loc::WSLCUserSettings_Warning_InvalidValue(L"credentialStore", s.SettingsFilePath().wstring(), 3), s.GetWarnings()[1].Message);
371 + // Values still fall back to built-in defaults.
372 + VERIFY_ARE_EQUAL(static_cast<int>(WSLCNetworkingModeVirtioProxy), static_cast<int>(s.Get<Setting::SessionNetworkingMode>()));
373 + VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
374 + }
375 +
376 + // credentialStore: invalid value must fall back to default and warn.
377 + TEST_METHOD(Validation_CredentialStore_Invalid_UsesDefaultAndWarns)
378 + {
379 + auto dir = UniqueTempDir();
380 + WriteFile(dir / L"settings.yaml", "credentialStore: badvalue\n");
381 +
382 + UserSettingsTest s{dir};
383 +
384 + VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
385 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
386 + VERIFY_ARE_EQUAL(
387 + Loc::WSLCUserSettings_Warning_InvalidValue(L"credentialStore", s.SettingsFilePath().wstring(), 1),
388 + s.GetWarnings().front().Message);
389 + }
390 +
391 + // -----------------------------------------------------------------------
392 + // Unknown key warnings
393 + // -----------------------------------------------------------------------
394 +
395 + // Unknown keys in a known section and unknown root sections both produce warnings.
396 + TEST_METHOD(Validation_UnknownKeys_WarnsAboutUnknownKeys)
397 + {
398 + auto dir = UniqueTempDir();
399 + WriteFile(
400 + dir / L"settings.yaml",
401 + "session:\n"
402 + " cpuCount: 4\n"
403 + " unknownSetting: 99\n"
404 + "unknownSection:\n"
405 + " foo: bar\n");
406 +
407 + UserSettingsTest s{dir};
408 +
409 + VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
410 + VERIFY_ARE_EQUAL(4u, s.Get<Setting::SessionCpuCount>());
411 + VERIFY_ARE_EQUAL(2u, s.GetWarnings().size());
412 + // Root-level keys are processed before nested keys due to stack-based traversal.
413 + VERIFY_ARE_EQUAL(
414 + Loc::WSLCUserSettings_Warning_UnknownSection(L"unknownSection", s.SettingsFilePath().wstring(), 4), s.GetWarnings()[0].Message);
415 + VERIFY_ARE_EQUAL(
416 + Loc::WSLCUserSettings_Warning_UnknownKey(L"session.unknownSetting", s.SettingsFilePath().wstring(), 3),
417 + s.GetWarnings()[1].Message);
418 + }
419 +
420 + // An unknown key under a known section produces a warning with the full path.
421 + TEST_METHOD(Validation_UnknownKeys_UnknownInKnownSection)
422 + {
423 + auto dir = UniqueTempDir();
424 + WriteFile(
425 + dir / L"settings.yaml",
426 + "session:\n"
427 + " cpuCount: 4\n"
428 + " typoSetting: true\n");
429 +
430 + UserSettingsTest s{dir};
431 +
432 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
433 + VERIFY_ARE_EQUAL(
434 + Loc::WSLCUserSettings_Warning_UnknownKey(L"session.typoSetting", s.SettingsFilePath().wstring(), 3),
435 + s.GetWarnings().front().Message);
436 + VERIFY_ARE_EQUAL(std::wstring(L"session.typoSetting"), s.GetWarnings().front().SettingPath);
437 + }
438 +
439 + // An unknown root-level section produces a warning.
440 + TEST_METHOD(Validation_UnknownKeys_UnknownRootSection)
441 + {
442 + auto dir = UniqueTempDir();
443 + WriteFile(
444 + dir / L"settings.yaml",
445 + "badSection:\n"
446 + " key: value\n");
447 +
448 + UserSettingsTest s{dir};
449 +
450 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
451 + VERIFY_ARE_EQUAL(
452 + Loc::WSLCUserSettings_Warning_UnknownSection(L"badSection", s.SettingsFilePath().wstring(), 1),
453 + s.GetWarnings().front().Message);
454 + VERIFY_ARE_EQUAL(std::wstring(L"badSection"), s.GetWarnings().front().SettingPath);
455 + }
456 +
457 + // An unknown root-level scalar key produces a warning.
458 + TEST_METHOD(Validation_UnknownKeys_UnknownRootScalar)
459 + {
460 + auto dir = UniqueTempDir();
461 + WriteFile(
462 + dir / L"settings.yaml",
463 + "session:\n"
464 + " cpuCount: 4\n"
465 + "badKey: hello\n");
466 +
467 + UserSettingsTest s{dir};
468 +
469 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
470 + VERIFY_ARE_EQUAL(
471 + Loc::WSLCUserSettings_Warning_UnknownKey(L"badKey", s.SettingsFilePath().wstring(), 3), s.GetWarnings().front().Message);
472 + VERIFY_ARE_EQUAL(std::wstring(L"badKey"), s.GetWarnings().front().SettingPath);
473 + }
474 +
475 + // A complex YAML key (sequence) cannot be converted to string;
476 + // a non-string key warning is emitted.
477 + TEST_METHOD(Validation_UnknownKeys_ComplexKey_WarnsNonStringKey)
478 + {
479 + auto dir = UniqueTempDir();
480 + WriteFile(
481 + dir / L"settings.yaml",
482 + "session:\n"
483 + " cpuCount: 4\n"
484 + " [1, 2]: value\n");
485 +
486 + UserSettingsTest s{dir};
487 +
488 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
489 + VERIFY_ARE_EQUAL(
490 + Loc::WSLCUserSettings_Warning_NonStringKey(L"session", s.SettingsFilePath().wstring(), 3), s.GetWarnings().front().Message);
491 + VERIFY_ARE_EQUAL(std::wstring(L"session"), s.GetWarnings().front().SettingPath);
492 + }
493 +
494 + // A complex YAML key (map) at root level warns with "root" location.
495 + TEST_METHOD(Validation_UnknownKeys_ComplexKeyAtRoot_WarnsNonStringKey)
496 + {
497 + auto dir = UniqueTempDir();
498 + WriteFile(
499 + dir / L"settings.yaml",
500 + "{a: b}: value\n"
501 + "credentialStore: wincred\n");
502 +
503 + UserSettingsTest s{dir};
504 +
505 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
506 + VERIFY_ARE_EQUAL(
507 + Loc::WSLCUserSettings_Warning_NonStringKey(L"root", s.SettingsFilePath().wstring(), 1), s.GetWarnings().front().Message);
508 + VERIFY_ARE_EQUAL(std::wstring(L"root"), s.GetWarnings().front().SettingPath);
509 + }
510 +
511 + // A file with only valid known keys produces no warnings.
512 + TEST_METHOD(Validation_UnknownKeys_AllKnownKeys_NoWarnings)
513 + {
514 + auto dir = UniqueTempDir();
515 + WriteFile(
516 + dir / L"settings.yaml",
517 + "session:\n"
518 + " cpuCount: 8\n"
519 + " memorySize: 4GB\n"
520 + " maxStorageSize: 50000MB\n"
521 + " networkingMode: nat\n"
522 + " hostFileShareMode: virtiofs\n"
523 + " dnsTunneling: true\n"
524 + "credentialStore: wincred\n");
525 +
526 + UserSettingsTest s{dir};
527 +
528 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
529 + }
530 +};
531 +
532 +} // namespace WSLCCLISettingsUnitTests
test/windows/wslc/WSLCCLITableOutputUnitTests.cpp new
+397
@@ -0,0 +1,397 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLITableOutputUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + Unit tests for the TableOutput class.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "WSLCCLITestHelpers.h"
18 +
19 +#include "TableOutput.h"
20 +
21 +using namespace wsl::windows::wslc;
22 +using namespace WSLCTestHelpers;
23 +using namespace WEX::Logging;
24 +using namespace WEX::Common;
25 +using namespace WEX::TestExecution;
26 +
27 +namespace WSLCTableOutputUnitTests {
28 +
29 +class WSLCTableOutputUnitTests
30 +{
31 + WSLC_TEST_CLASS(WSLCTableOutputUnitTests)
32 +
33 + TEST_CLASS_SETUP(TestClassSetup)
34 + {
35 + return true;
36 + }
37 +
38 + TEST_CLASS_CLEANUP(TestClassCleanup)
39 + {
40 + return true;
41 + }
42 +
43 + // Test: header line is emitted as the first row, even with no data rows.
44 + TEST_METHOD(TableOutput_AlwaysShowHeader_EmitsHeaderWhenEmpty)
45 + {
46 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
47 + cap.table.SetAlwaysShowHeader(true);
48 +
49 + cap.table.Complete();
50 +
51 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines.size());
52 + // Header line must contain both column names
53 + VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
54 + VERIFY_IS_TRUE(cap.lines[0].find(L"STATUS") != std::wstring::npos);
55 + }
56 +
57 + // Test: no output at all when empty and AlwaysShowHeader is false.
58 + TEST_METHOD(TableOutput_NoHeader_EmitsNothingWhenEmpty)
59 + {
60 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
61 + cap.table.SetAlwaysShowHeader(false);
62 +
63 + cap.table.Complete();
64 +
65 + VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines.size());
66 + }
67 +
68 + // Test: one data row produces header + one data line.
69 + TEST_METHOD(TableOutput_SingleRow_EmitsHeaderPlusOneDataLine)
70 + {
71 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
72 +
73 + cap.table.OutputLine({L"my-container", L"running"});
74 + cap.table.Complete();
75 +
76 + // Expect: header row + 1 data row = 2 lines total
77 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
78 + VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
79 + VERIFY_IS_TRUE(cap.lines[1].find(L"my-container") != std::wstring::npos);
80 + VERIFY_IS_TRUE(cap.lines[1].find(L"running") != std::wstring::npos);
81 + }
82 +
83 + // Test: multiple data rows all appear after the header.
84 + TEST_METHOD(TableOutput_MultipleRows_AllRowsEmittedAfterHeader)
85 + {
86 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
87 +
88 + cap.table.OutputLine({L"container-a", L"running"});
89 + cap.table.OutputLine({L"container-b", L"stopped"});
90 + cap.table.OutputLine({L"container-c", L"paused"});
91 + cap.table.Complete();
92 +
93 + VERIFY_ARE_EQUAL(static_cast<size_t>(4), cap.lines.size()); // header + 3 rows
94 +
95 + VERIFY_IS_TRUE(cap.lines[1].find(L"container-a") != std::wstring::npos);
96 + VERIFY_IS_TRUE(cap.lines[2].find(L"container-b") != std::wstring::npos);
97 + VERIFY_IS_TRUE(cap.lines[3].find(L"container-c") != std::wstring::npos);
98 + }
99 +
100 + // Test: columns are separated by the correct number of spaces.
101 + TEST_METHOD(TableOutput_ColumnPadding_DefaultPaddingApplied)
102 + {
103 + // Use a custom padding of 3 (the default) and verify the data row
104 + // contains at least 3 spaces between the first column value and the
105 + // start of the second column value.
106 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, /*sizingBuffer=*/50, /*columnPadding=*/3);
107 +
108 + cap.table.OutputLine({L"abc", L"ok"});
109 + cap.table.Complete();
110 +
111 + // Data row: "abc" padded to header width ("NAME"=4) + 3 spaces, then "ok"
112 + // Expected: "abc ok" with appropriate spacing
113 + const std::wstring& dataLine = cap.lines[1];
114 + VERIFY_IS_TRUE(dataLine.find(L"abc") != std::wstring::npos);
115 + VERIFY_IS_TRUE(dataLine.find(L"ok") != std::wstring::npos);
116 +
117 + // There must be at least 3 spaces between the two values
118 + auto columnPadding = 3;
119 + auto namePos = dataLine.find(L"abc");
120 + auto statusPos = dataLine.find(L"ok");
121 + VERIFY_IS_TRUE(statusPos >= namePos + wcslen(L"abc") + columnPadding);
122 + }
123 +
124 + // Test: custom column padding is respected.
125 + TEST_METHOD(TableOutput_ColumnPadding_CustomPaddingApplied)
126 + {
127 + constexpr size_t customPadding = 5;
128 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"A", L"B"}, /*sizingBuffer=*/50, customPadding);
129 +
130 + cap.table.OutputLine({L"x", L"y"});
131 + cap.table.Complete();
132 +
133 + // "A" header is 1 char wide, "x" value is 1 char wide.
134 + // With 5-space padding, "y" must start at position >= 1 + 5 = 6.
135 + const std::wstring& dataLine = cap.lines[1];
136 + auto posX = dataLine.find(L'x');
137 + auto posY = dataLine.find(L'y');
138 + VERIFY_IS_TRUE(posX != std::wstring::npos);
139 + VERIFY_IS_TRUE(posY != std::wstring::npos);
140 + VERIFY_IS_TRUE(posY >= posX + 1 + customPadding);
141 + }
142 +
143 + // Test: column width expands to fit the widest data value.
144 + TEST_METHOD(TableOutput_ColumnWidth_ExpandsToFitData)
145 + {
146 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"ID", L"NAME"});
147 +
148 + cap.table.OutputLine({L"1", L"short"});
149 + cap.table.OutputLine({L"2", L"a-very-long-container-name"});
150 + cap.table.Complete();
151 +
152 + // The second column must accommodate the widest value in every row.
153 + for (size_t i = 1; i < cap.lines.size(); ++i)
154 + {
155 + // The long value must not have been truncated.
156 + if (cap.lines[i].find(L"a-very-long-container-name") != std::wstring::npos)
157 + {
158 + LogComment(L"Long value found intact in row " + std::to_wstring(i));
159 + }
160 + }
161 + VERIFY_IS_TRUE(cap.lines[2].find(L"a-very-long-container-name") != std::wstring::npos);
162 + }
163 +
164 + // Test: column width is at least as wide as the header.
165 + TEST_METHOD(TableOutput_ColumnWidth_AtLeastHeaderWidth)
166 + {
167 + // Header "CONTAINER_NAME" is 14 chars; data value is only 3 chars.
168 + // The data line must still be padded to the header width.
169 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"CONTAINER_NAME", L"ST"});
170 +
171 + cap.table.OutputLine({L"abc", L"ok"});
172 + cap.table.Complete();
173 +
174 + // Header line: "CONTAINER_NAME" starts at position 0.
175 + // Data line: "abc" starts at position 0, "ok" must not start before
176 + // position 14 + padding.
177 + const std::wstring& dataLine = cap.lines[1];
178 + auto posOk = dataLine.find(L"ok");
179 + VERIFY_IS_TRUE(posOk != std::wstring::npos);
180 + // "CONTAINER_NAME" = 14 chars, padding = 3 -> "ok" must be at >= 17
181 + VERIFY_IS_TRUE(posOk >= static_cast<size_t>(14 + TableOutput<2>::DefaultColumnPadding));
182 + }
183 +
184 + // Test: values exceeding MaxWidth are truncated and an ellipsis appended.
185 + TEST_METHOD(TableOutput_MaxWidth_LongValueIsTruncatedWithEllipsis)
186 + {
187 + TableOutput<2>::column_config_t configs{};
188 + configs[0].MaxWidth = 8; // limit first column to 8 chars
189 + configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
190 +
191 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, std::move(configs));
192 +
193 + cap.table.OutputLine({L"a-very-long-name", L"running"});
194 + cap.table.Complete();
195 +
196 + const std::wstring& dataLine = cap.lines[1];
197 + // Ellipsis character (U+2026) must be present
198 + VERIFY_IS_TRUE(dataLine.find(L"\x2026") != std::wstring::npos);
199 + // Full original value must NOT be present
200 + VERIFY_IS_TRUE(dataLine.find(L"a-very-long-name") == std::wstring::npos);
201 + }
202 +
203 + // Test: values within MaxWidth are not truncated.
204 + TEST_METHOD(TableOutput_MaxWidth_ShortValueNotTruncated)
205 + {
206 + TableOutput<2>::column_config_t configs{};
207 + configs[0].MaxWidth = 20;
208 + configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
209 +
210 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, std::move(configs));
211 +
212 + cap.table.OutputLine({L"short", L"running"});
213 + cap.table.Complete();
214 +
215 + const std::wstring& dataLine = cap.lines[1];
216 + VERIFY_IS_TRUE(dataLine.find(L"short") != std::wstring::npos);
217 + VERIFY_IS_TRUE(dataLine.find(L"\x2026") == std::wstring::npos);
218 + }
219 +
220 + // Test: columns shrink when total width exceeds console width.
221 + TEST_METHOD(TableOutput_ConsoleWidthLimit_PreferredShrinkColumnIsShrunk)
222 + {
223 + // Two columns, first marked preferredShrink=false, second preferredShrink=true.
224 + // With a very narrow console the second column should absorb the cut.
225 + TableOutput<2>::column_config_t configs{};
226 + configs[0].MaxWidth = ColumnWidthConfig::NoLimit;
227 + configs[0].PreferredShrink = false;
228 + configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
229 + configs[1].PreferredShrink = true;
230 +
231 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"ID", L"DESCRIPTION"}, std::move(configs));
232 + // Override with a very narrow console: only 20 chars wide.
233 + cap.table.SetConsoleWidthOverride(20);
234 + cap.table.SetColumnWidthLimiting(true);
235 +
236 + cap.table.OutputLine({L"abc123", L"this-is-a-long-description-value"});
237 + cap.table.Complete();
238 +
239 + // The output must fit within 20 chars.
240 + for (const auto& line : cap.lines)
241 + {
242 + VERIFY_IS_TRUE(line.size() <= static_cast<size_t>(20));
243 + }
244 + }
245 +
246 + // Test: IsEmpty returns true before any rows are added, and false after a row is added.
247 + TEST_METHOD(TableOutput_IsEmpty)
248 + {
249 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
250 + VERIFY_IS_TRUE(cap.table.IsEmpty());
251 +
252 + cap.table.OutputLine({L"foo", L"bar"});
253 + VERIFY_IS_FALSE(cap.table.IsEmpty());
254 + }
255 +
256 + // Test: column-definition constructor wires up names and configs correctly.
257 + TEST_METHOD(TableOutput_ColumnDefinition_NameAndConfigUsed)
258 + {
259 + TableOutput<2>::column_def_t defs{{
260 + ColumnDefinition{L"MYID", {ColumnWidthConfig::NoLimit, 6, false}},
261 + ColumnDefinition{L"MYNAME", {ColumnWidthConfig::NoLimit, ColumnWidthConfig::NoLimit, true}},
262 + }};
263 +
264 + TableOutputCapture<2> cap(std::move(defs));
265 +
266 + cap.table.OutputLine({L"id-value", L"name-value"});
267 + cap.table.Complete();
268 +
269 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
270 + VERIFY_IS_TRUE(cap.lines[0].find(L"MYID") != std::wstring::npos);
271 + VERIFY_IS_TRUE(cap.lines[0].find(L"MYNAME") != std::wstring::npos);
272 +
273 + // "id-value" is 8 chars but MaxWidth=6 -> must be truncated
274 + VERIFY_IS_TRUE(cap.lines[1].find(L"\x2026") != std::wstring::npos);
275 + }
276 +
277 + // Test: SetShowHeader(false) suppresses header when there are data rows.
278 + TEST_METHOD(TableOutput_ShowHeader_False_SuppressesHeaderWithDataRows)
279 + {
280 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
281 + cap.table.SetShowHeader(false);
282 +
283 + cap.table.OutputLine({L"my-container", L"running"});
284 + cap.table.Complete();
285 +
286 + // Only the data row should be emitted.
287 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines.size());
288 + VERIFY_IS_TRUE(cap.lines[0].find(L"my-container") != std::wstring::npos);
289 + VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
290 + }
291 +
292 + // Test: SetShowHeader(false) with AlwaysShowHeader(true) still suppresses header when empty.
293 + TEST_METHOD(TableOutput_ShowHeader_False_SuppressesHeaderEvenWhenAlwaysShowHeaderTrue)
294 + {
295 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
296 + cap.table.SetAlwaysShowHeader(true);
297 + cap.table.SetShowHeader(false);
298 +
299 + cap.table.Complete();
300 +
301 + // SetShowHeader(false) takes precedence. Nothing should be emitted.
302 + VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines.size());
303 + }
304 +
305 + // Test: SetShowHeader(true) is the default. Header appears before data rows.
306 + TEST_METHOD(TableOutput_ShowHeader_True_IsDefaultAndEmitsHeader)
307 + {
308 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
309 + // No explicit call to SetShowHeader. Default must be true.
310 +
311 + cap.table.OutputLine({L"my-container", L"running"});
312 + cap.table.Complete();
313 +
314 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
315 + VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
316 + VERIFY_IS_TRUE(cap.lines[0].find(L"STATUS") != std::wstring::npos);
317 + }
318 +
319 + // Test: SetShowHeader(false) with multiple data rows emits only data rows.
320 + TEST_METHOD(TableOutput_ShowHeader_False_MultipleDataRowsNoHeader)
321 + {
322 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
323 + cap.table.SetShowHeader(false);
324 +
325 + cap.table.OutputLine({L"container-a", L"running"});
326 + cap.table.OutputLine({L"container-b", L"stopped"});
327 + cap.table.Complete();
328 +
329 + // Two data rows, zero header rows.
330 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
331 + VERIFY_IS_TRUE(cap.lines[0].find(L"container-a") != std::wstring::npos);
332 + VERIFY_IS_TRUE(cap.lines[1].find(L"container-b") != std::wstring::npos);
333 + // Neither line should contain the column header text.
334 + VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
335 + VERIFY_IS_TRUE(cap.lines[1].find(L"NAME") == std::wstring::npos);
336 + }
337 +
338 + // Test: SetShowHeader controls whether the header row is emitted.
339 + // Covers: default (true), suppression with data rows, suppression when empty
340 + // (even with AlwaysShowHeader), and multiple data rows with no header.
341 + TEST_METHOD(TableOutput_ShowHeader)
342 + {
343 + // Default is true. Header appears before data rows without an explicit call.
344 + {
345 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
346 +
347 + cap.table.OutputLine({L"my-container", L"running"});
348 + cap.table.Complete();
349 +
350 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
351 + VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
352 + VERIFY_IS_TRUE(cap.lines[0].find(L"STATUS") != std::wstring::npos);
353 + }
354 +
355 + // SetShowHeader(false) suppresses the header when data rows are present.
356 + {
357 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
358 + cap.table.SetShowHeader(false);
359 +
360 + cap.table.OutputLine({L"my-container", L"running"});
361 + cap.table.Complete();
362 +
363 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines.size());
364 + VERIFY_IS_TRUE(cap.lines[0].find(L"my-container") != std::wstring::npos);
365 + VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
366 + }
367 +
368 + // SetShowHeader(false) suppresses the header even when AlwaysShowHeader is true and the table is empty.
369 + {
370 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
371 + cap.table.SetAlwaysShowHeader(true);
372 + cap.table.SetShowHeader(false);
373 +
374 + cap.table.Complete();
375 +
376 + VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines.size());
377 + }
378 +
379 + // SetShowHeader(false) with multiple data rows emits only data rows.
380 + {
381 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
382 + cap.table.SetShowHeader(false);
383 +
384 + cap.table.OutputLine({L"container-a", L"running"});
385 + cap.table.OutputLine({L"container-b", L"stopped"});
386 + cap.table.Complete();
387 +
388 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
389 + VERIFY_IS_TRUE(cap.lines[0].find(L"container-a") != std::wstring::npos);
390 + VERIFY_IS_TRUE(cap.lines[1].find(L"container-b") != std::wstring::npos);
391 + VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
392 + VERIFY_IS_TRUE(cap.lines[1].find(L"NAME") == std::wstring::npos);
393 + }
394 + }
395 +};
396 +
397 +} // namespace WSLCTableOutputUnitTests
\ No newline at end of file
test/windows/wslc/WSLCCLITestHelpers.h new
+81
@@ -0,0 +1,81 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCTestHelpers.h
8 +
9 +Abstract:
10 +
11 + Helper utilities for WSLC CLI unit tests.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include <string>
18 +#include <Windows.h>
19 +#include <WexTestClass.h>
20 +#include "Invocation.h"
21 +#include "TableOutput.h"
22 +
23 +namespace WSLCTestHelpers {
24 +
25 +inline wsl::windows::wslc::Invocation CreateInvocationFromCommandLine(const std::wstring& commandLine)
26 +{
27 + // Simulate creation of Arvc/Argc from command line as Windows does.
28 + int argc = 0;
29 + wil::unique_hlocal_ptr<LPWSTR[]> argv;
30 + argv.reset(CommandLineToArgvW(commandLine.c_str(), &argc));
31 + VERIFY_IS_NOT_NULL(argv.get());
32 + VERIFY_IS_GREATER_THAN(argc, 0);
33 +
34 + // Convert to vector for Invocation, skipping argv[0] (executable path)
35 + // This is what we do in wmain() to populate Invocation input vector.
36 + std::vector<std::wstring> args;
37 + for (int i = 1; i < argc; ++i) // Skip argv[0]
38 + {
39 + args.push_back(argv[i]);
40 + }
41 +
42 + return wsl::windows::wslc::Invocation(std::move(args));
43 +}
44 +
45 +// Helper function to convert wstring to UTF-8 string for TAEF logging
46 +inline std::string WStringToUTF8(const std::wstring& wstr)
47 +{
48 + if (wstr.empty())
49 + {
50 + return std::string();
51 + }
52 +
53 + int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), static_cast<int>(wstr.size()), nullptr, 0, nullptr, nullptr);
54 + std::string result(size_needed, 0);
55 + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), static_cast<int>(wstr.size()), &result[0], size_needed, nullptr, nullptr);
56 + return result;
57 +}
58 +
59 +// Convenience wrapper for Log::Comment with wstring
60 +inline void LogComment(const std::wstring& message)
61 +{
62 + WEX::Logging::Log::Comment(reinterpret_cast<const char8_t*>(WStringToUTF8(message).c_str()));
63 +}
64 +
65 +// Helper: capture all lines emitted by a TableOutput into a vector<wstring>.
66 +template <size_t N>
67 +struct TableOutputCapture
68 +{
69 + std::vector<std::wstring> lines;
70 + wsl::windows::wslc::TableOutput<N> table;
71 +
72 + // Forwards constructor arguments straight to TableOutput.
73 + template <typename... Args>
74 + explicit TableOutputCapture(Args&&... args) : table(std::forward<Args>(args)...)
75 + {
76 + table.SetOutputFunction([this](const std::wstring& line) { lines.push_back(line); });
77 + // Pin the console width so shrinking tests are deterministic.
78 + table.SetConsoleWidthOverride(120);
79 + }
80 +};
81 +} // namespace WSLCTestHelpers
\ No newline at end of file
test/windows/wslc/WSLCCLITmpfsParserUnitTests.cpp new
+51
@@ -0,0 +1,51 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLITmpfsParserUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC CLI tmpfs parsing and validation.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCCLITestHelpers.h"
17 +#include "ContainerModel.h"
18 +
19 +using namespace wsl::windows::wslc;
20 +
21 +namespace WSLCCLITmpfsParserUnitTests {
22 +
23 +class WSLCCLITmpfsParserUnitTests
24 +{
25 + WSLC_TEST_CLASS(WSLCCLITmpfsParserUnitTests)
26 +
27 + TEST_METHOD(WSLCCLITmpfsMount_Parse)
28 + {
29 + std::vector<std::tuple<std::string, std::string, std::string>> validTmpfsSpecs = {
30 + {"", "", ""},
31 + {"/tmp", "/tmp", ""},
32 + {"/tmp:size=50m", "/tmp", "size=50m"},
33 + {"/var/tmp:size=1g", "/var/tmp", "size=1g"},
34 + {"/tmp:size=50m,mode=1777", "/tmp", "size=50m,mode=1777"},
35 + {"/cache:uid=1000,gid=1000", "/cache", "uid=1000,gid=1000"},
36 + {"/mnt/ramdisk:size=256k,nr_inodes=1k", "/mnt/ramdisk", "size=256k,nr_inodes=1k"},
37 + {"/securetmp:mode=0700", "/securetmp", "mode=0700"},
38 + {"/scratch:nosuid,nodev,noexec", "/scratch", "nosuid,nodev,noexec"},
39 + {"/wsl/tmp:size=2g,uid=0,gid=0,mode=1777", "/wsl/tmp", "size=2g,uid=0,gid=0,mode=1777"},
40 + };
41 +
42 + for (const auto& [input, expectedContainerPath, expectedOptions] : validTmpfsSpecs)
43 + {
44 + auto result = models::TmpfsMount::Parse(input);
45 + VERIFY_ARE_EQUAL(expectedContainerPath, result.ContainerPath());
46 + VERIFY_ARE_EQUAL(expectedOptions, result.Options());
47 + }
48 + }
49 +};
50 +
51 +} // namespace WSLCCLITmpfsParserUnitTests
\ No newline at end of file
test/windows/wslc/WSLCPortParserUnitTests.cpp new
+391
@@ -0,0 +1,391 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCPortParserUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC port argument parsing and validation.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCCLITestHelpers.h"
17 +
18 +#include <ContainerModel.h>
19 +
20 +namespace WSLCPortParserUnitTests {
21 +
22 +using namespace wsl::windows::wslc::models;
23 +
24 +class WSLCPortParserUnitTests
25 +{
26 + WSL_TEST_CLASS(WSLCPortParserUnitTests)
27 +
28 + TEST_CLASS_SETUP(TestClassSetup)
29 + {
30 + return true;
31 + }
32 +
33 + TEST_CLASS_CLEANUP(TestClassCleanup)
34 + {
35 + return true;
36 + }
37 +
38 + TEST_METHOD(PortParserTest_HostAndContainerPort_Valid)
39 + {
40 + auto result = PublishPort::Parse("8080:80");
41 +
42 + VerifyParseState(result, "8080:80", false, false);
43 + VerifyNoHostIP(result);
44 + VerifyHostPort(result, 8080, 8080);
45 + VerifyContainerPort(result, 80, 80);
46 + VerifyProtocol(result, PublishPort::Protocol::TCP);
47 + }
48 +
49 + TEST_METHOD(PortParserTest_ContainerPort_Only_Valid)
50 + {
51 + auto result = PublishPort::Parse("80");
52 +
53 + VerifyParseState(result, "80", true, false);
54 + VerifyNoHostIP(result);
55 + VerifyEphemeralHostPort(result);
56 + VerifyContainerPort(result, 80, 80);
57 + VerifyProtocol(result, PublishPort::Protocol::TCP);
58 + }
59 +
60 + TEST_METHOD(PortParserTest_ContainerPort_WithProtocol_NoIP)
61 + {
62 + {
63 + auto result = PublishPort::Parse("80/tcp");
64 +
65 + VerifyParseState(result, "80/tcp", true, false);
66 + VerifyNoHostIP(result);
67 + VerifyEphemeralHostPort(result);
68 + VerifyContainerPort(result, 80, 80);
69 + VerifyProtocol(result, PublishPort::Protocol::TCP);
70 + }
71 +
72 + {
73 + auto result = PublishPort::Parse("54/udp");
74 +
75 + VerifyParseState(result, "54/udp", true, false);
76 + VerifyNoHostIP(result);
77 + VerifyEphemeralHostPort(result);
78 + VerifyContainerPort(result, 54, 54);
79 + VerifyProtocol(result, PublishPort::Protocol::UDP);
80 + }
81 + }
82 +
83 + TEST_METHOD(PortParserTest_ContainerPortRange_Only_Valid)
84 + {
85 + auto result = PublishPort::Parse("8000-8005");
86 +
87 + VerifyParseState(result, "8000-8005", true, true);
88 + VerifyNoHostIP(result);
89 + VerifyEphemeralHostPort(result);
90 + VerifyContainerPort(result, 8000, 8005);
91 + VerifyProtocol(result, PublishPort::Protocol::TCP);
92 + }
93 +
94 + TEST_METHOD(PortParserTest_ContainerPortRange_WithProtocol_NoIP)
95 + {
96 + auto result = PublishPort::Parse("8000-8005/udp");
97 +
98 + VerifyParseState(result, "8000-8005/udp", true, true);
99 + VerifyNoHostIP(result);
100 + VerifyEphemeralHostPort(result);
101 + VerifyContainerPort(result, 8000, 8005);
102 + VerifyProtocol(result, PublishPort::Protocol::UDP);
103 + }
104 +
105 + TEST_METHOD(PortParserTest_IPv4Mappings_Valid)
106 + {
107 + {
108 + auto result = PublishPort::Parse("127.0.0.1:8080:80");
109 +
110 + VerifyParseState(result, "127.0.0.1:8080:80", false, false);
111 + VerifyHostIPv4(result, "127.0.0.1");
112 + VerifyHostPort(result, 8080, 8080);
113 + VerifyContainerPort(result, 80, 80);
114 + VerifyProtocol(result, PublishPort::Protocol::TCP);
115 + }
116 +
117 + {
118 + auto result = PublishPort::Parse("0.0.0.0:8080:80");
119 +
120 + VerifyParseState(result, "0.0.0.0:8080:80", false, false);
121 + VerifyHostIPv4(result, "0.0.0.0");
122 + VerifyHostPort(result, 8080, 8080);
123 + VerifyContainerPort(result, 80, 80);
124 + VerifyProtocol(result, PublishPort::Protocol::TCP);
125 + }
126 +
127 + {
128 + auto result = PublishPort::Parse("192.168.1.50:8080:80");
129 +
130 + VerifyParseState(result, "192.168.1.50:8080:80", false, false);
131 + VerifyHostIPv4(result, "192.168.1.50");
132 + VerifyHostPort(result, 8080, 8080);
133 + VerifyContainerPort(result, 80, 80);
134 + VerifyProtocol(result, PublishPort::Protocol::TCP);
135 + }
136 +
137 + {
138 + auto result = PublishPort::Parse("127.0.0.1:5353:5353/udp");
139 +
140 + VerifyParseState(result, "127.0.0.1:5353:5353/udp", false, false);
141 + VerifyHostIPv4(result, "127.0.0.1");
142 + VerifyHostPort(result, 5353, 5353);
143 + VerifyContainerPort(result, 5353, 5353);
144 + VerifyProtocol(result, PublishPort::Protocol::UDP);
145 + }
146 +
147 + {
148 + auto result = PublishPort::Parse("127.0.0.1::80");
149 +
150 + VerifyParseState(result, "127.0.0.1::80", true, false);
151 + VerifyHostIPv4(result, "127.0.0.1");
152 + VerifyEphemeralHostPort(result);
153 + VerifyContainerPort(result, 80, 80);
154 + VerifyProtocol(result, PublishPort::Protocol::TCP);
155 + }
156 +
157 + {
158 + auto result = PublishPort::Parse("127.0.0.1::53/udp");
159 +
160 + VerifyParseState(result, "127.0.0.1::53/udp", true, false);
161 + VerifyHostIPv4(result, "127.0.0.1");
162 + VerifyEphemeralHostPort(result);
163 + VerifyContainerPort(result, 53, 53);
164 + VerifyProtocol(result, PublishPort::Protocol::UDP);
165 + }
166 + }
167 +
168 + TEST_METHOD(PortParserTest_IPv6Mappings_Valid)
169 + {
170 + {
171 + auto result = PublishPort::Parse("[::1]:8080:80");
172 +
173 + VerifyParseState(result, "[::1]:8080:80", false, false);
174 + VerifyHostIPv6(result, "::1");
175 + VerifyHostPort(result, 8080, 8080);
176 + VerifyContainerPort(result, 80, 80);
177 + VerifyProtocol(result, PublishPort::Protocol::TCP);
178 + }
179 +
180 + {
181 + auto result = PublishPort::Parse("[::]:8080:80");
182 +
183 + VerifyParseState(result, "[::]:8080:80", false, false);
184 + VerifyHostIPv6(result, "::");
185 + VerifyHostPort(result, 8080, 8080);
186 + VerifyContainerPort(result, 80, 80);
187 + VerifyProtocol(result, PublishPort::Protocol::TCP);
188 + }
189 +
190 + {
191 + auto result = PublishPort::Parse("[2001:db8::10]:8080:80");
192 +
193 + VerifyParseState(result, "[2001:db8::10]:8080:80", false, false);
194 + VerifyHostIPv6(result, "2001:db8::10");
195 + VerifyHostPort(result, 8080, 8080);
196 + VerifyContainerPort(result, 80, 80);
197 + VerifyProtocol(result, PublishPort::Protocol::TCP);
198 + }
199 +
200 + {
201 + auto result = PublishPort::Parse("[::1]:5353:5353/udp");
202 +
203 + VerifyParseState(result, "[::1]:5353:5353/udp", false, false);
204 + VerifyHostIPv6(result, "::1");
205 + VerifyHostPort(result, 5353, 5353);
206 + VerifyContainerPort(result, 5353, 5353);
207 + VerifyProtocol(result, PublishPort::Protocol::UDP);
208 + }
209 +
210 + {
211 + auto result = PublishPort::Parse("[::1]::80");
212 +
213 + VerifyParseState(result, "[::1]::80", true, false);
214 + VerifyHostIPv6(result, "::1");
215 + VerifyEphemeralHostPort(result);
216 + VerifyContainerPort(result, 80, 80);
217 + VerifyProtocol(result, PublishPort::Protocol::TCP);
218 + }
219 +
220 + {
221 + auto result = PublishPort::Parse("[::]::53/udp");
222 +
223 + VerifyParseState(result, "[::]::53/udp", true, false);
224 + VerifyHostIPv6(result, "::");
225 + VerifyEphemeralHostPort(result);
226 + VerifyContainerPort(result, 53, 53);
227 + VerifyProtocol(result, PublishPort::Protocol::UDP);
228 + }
229 + }
230 +
231 + TEST_METHOD(PortParserTest_PortRangeMappings_Valid)
232 + {
233 + {
234 + auto result = PublishPort::Parse("8000-8005:8000-8005");
235 +
236 + VerifyParseState(result, "8000-8005:8000-8005", false, true);
237 + VerifyNoHostIP(result);
238 + VerifyHostPort(result, 8000, 8005);
239 + VerifyContainerPort(result, 8000, 8005);
240 + VerifyProtocol(result, PublishPort::Protocol::TCP);
241 + }
242 +
243 + {
244 + auto result = PublishPort::Parse("127.0.0.1:9000-9003:9000-9003");
245 +
246 + VerifyParseState(result, "127.0.0.1:9000-9003:9000-9003", false, true);
247 + VerifyHostIPv4(result, "127.0.0.1");
248 + VerifyHostPort(result, 9000, 9003);
249 + VerifyContainerPort(result, 9000, 9003);
250 + VerifyProtocol(result, PublishPort::Protocol::TCP);
251 + }
252 +
253 + {
254 + auto result = PublishPort::Parse("[::1]:7000-7002:7000-7002");
255 +
256 + VerifyParseState(result, "[::1]:7000-7002:7000-7002", false, true);
257 + VerifyHostIPv6(result, "::1");
258 + VerifyHostPort(result, 7000, 7002);
259 + VerifyContainerPort(result, 7000, 7002);
260 + VerifyProtocol(result, PublishPort::Protocol::TCP);
261 + }
262 +
263 + {
264 + auto result = PublishPort::Parse("10000-10010:10000-10010/tcp");
265 +
266 + VerifyParseState(result, "10000-10010:10000-10010/tcp", false, true);
267 + VerifyNoHostIP(result);
268 + VerifyHostPort(result, 10000, 10010);
269 + VerifyContainerPort(result, 10000, 10010);
270 + VerifyProtocol(result, PublishPort::Protocol::TCP);
271 + }
272 +
273 + {
274 + auto result = PublishPort::Parse("20000-20002:20000-20002/udp");
275 +
276 + VerifyParseState(result, "20000-20002:20000-20002/udp", false, true);
277 + VerifyNoHostIP(result);
278 + VerifyHostPort(result, 20000, 20002);
279 + VerifyContainerPort(result, 20000, 20002);
280 + VerifyProtocol(result, PublishPort::Protocol::UDP);
281 + }
282 + }
283 +
284 + TEST_METHOD(PortParserTest_EphemeralHostPort_WithRange_AndIP_Valid)
285 + {
286 + {
287 + auto result = PublishPort::Parse("127.0.0.1::8000-8005");
288 +
289 + VerifyParseState(result, "127.0.0.1::8000-8005", true, true);
290 + VerifyHostIPv4(result, "127.0.0.1");
291 + VerifyEphemeralHostPort(result);
292 + VerifyContainerPort(result, 8000, 8005);
293 + VerifyProtocol(result, PublishPort::Protocol::TCP);
294 + }
295 +
296 + {
297 + auto result = PublishPort::Parse("[::1]::8000-8005");
298 +
299 + VerifyParseState(result, "[::1]::8000-8005", true, true);
300 + VerifyHostIPv6(result, "::1");
301 + VerifyEphemeralHostPort(result);
302 + VerifyContainerPort(result, 8000, 8005);
303 + VerifyProtocol(result, PublishPort::Protocol::TCP);
304 + }
305 + }
306 +
307 + TEST_METHOD(PortParserTest_InvalidMappings)
308 + {
309 + static const std::vector<std::string> invalidCases = {
310 + "", // Empty input
311 + " ", // Whitespace only
312 + "80 ", // Trailing whitespace
313 + ":80", // Empty host port
314 + "127.0.0.1:80", // Missing container port
315 + "[::1]:8080", // Missing container port
316 + "8000-8005:8000-8006", // Mismatched port ranges
317 + "8000-8005:8000", // Mismatched port ranges
318 + "8000:8000-8005", // Mismatched port ranges
319 + "8080:80/icmp", // Invalid protocol
320 + "8080:80/udpp", // Invalid protocol
321 + "80/TCP", // Protocol is case sensitive
322 + "80/tcp:90", // Protocol suffix must be final
323 + "80-", // Malformed port range
324 + "-80", // Malformed port range
325 + "80--81", // Malformed port range
326 + "8000-7000", // Invalid port range
327 + "0", // Invalid port number
328 + "65536", // Invalid port number
329 + };
330 +
331 + for (const auto& value : invalidCases)
332 + {
333 + VERIFY_THROWS(PublishPort::Parse(value), wil::ResultException);
334 + }
335 + }
336 +
337 +private:
338 + static void VerifyRange(const PublishPort::PortRange& range, int start, int end)
339 + {
340 + VERIFY_ARE_EQUAL(start, range.Start());
341 + VERIFY_ARE_EQUAL(end, range.End());
342 + VERIFY_ARE_EQUAL(start == end, range.IsSingle());
343 + }
344 +
345 + static void VerifyParseState(const PublishPort& result, const std::string& original, bool hasEphemeralHostPort, bool isRangeMapping)
346 + {
347 + VERIFY_ARE_EQUAL(original, result.Original());
348 + VERIFY_ARE_EQUAL(hasEphemeralHostPort, result.HostPort().IsEphemeral());
349 + VERIFY_ARE_EQUAL(isRangeMapping, result.IsRangeMapping());
350 + }
351 +
352 + static void VerifyProtocol(const PublishPort& result, PublishPort::Protocol protocol)
353 + {
354 + VERIFY_ARE_EQUAL(static_cast<int>(protocol), static_cast<int>(result.PortProtocol()));
355 + }
356 +
357 + static void VerifyNoHostIP(const PublishPort& result)
358 + {
359 + VERIFY_IS_FALSE(result.HostIP().has_value());
360 + }
361 +
362 + static void VerifyEphemeralHostPort(const PublishPort& result)
363 + {
364 + VERIFY_IS_TRUE(result.HostPort().IsEphemeral());
365 + }
366 +
367 + static void VerifyHostPort(const PublishPort& result, int start, int end)
368 + {
369 + VerifyRange(result.HostPort(), start, end);
370 + }
371 +
372 + static void VerifyContainerPort(const PublishPort& result, int start, int end)
373 + {
374 + VerifyRange(result.ContainerPort(), start, end);
375 + }
376 +
377 + static void VerifyHostIPv4(const PublishPort& result, const std::string& expectedString)
378 + {
379 + VERIFY_IS_TRUE(result.HostIP().has_value());
380 + VERIFY_IS_FALSE(result.HostIP()->IsIPv6());
381 + VERIFY_ARE_EQUAL(expectedString, result.HostIP()->IP());
382 + }
383 +
384 + static void VerifyHostIPv6(const PublishPort& result, const std::string& expectedString)
385 + {
386 + VERIFY_IS_TRUE(result.HostIP().has_value());
387 + VERIFY_IS_TRUE(result.HostIP()->IsIPv6());
388 + VERIFY_ARE_EQUAL(expectedString, result.HostIP()->IP());
389 + }
390 +};
391 +} // namespace WSLCPortParserUnitTests
test/windows/wslc/WSLCVolumeMountUnitTests.cpp new
+146
@@ -0,0 +1,146 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCVolumeMountUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC volume mount argument parsing and validation.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "ContainerModel.h"
18 +
19 +using namespace wsl::windows::wslc::models;
20 +
21 +namespace WSLCVolumeMount {
22 +
23 +class WSLCVolumeMountUnitTests
24 +{
25 + WSL_TEST_CLASS(WSLCVolumeMountUnitTests)
26 +
27 + TEST_METHOD(VolumeMount_Parse_ReturnExpectedResult)
28 + {
29 + const auto cwd = std::filesystem::current_path();
30 +
31 + // Volume value => host, container, readonly
32 + std::vector<std::tuple<std::wstring, std::wstring, std::string, bool>> validVolumeArgs = {
33 + {LR"(C:\hostPath:/containerPath)", LR"(C:\hostPath)", R"(/containerPath)", false},
34 + {LR"(C:\hostPath:/containerPath:ro)", LR"(C:\hostPath)", R"(/containerPath)", true},
35 + {LR"(C:\hostPath:/containerPath:rw)", LR"(C:\hostPath)", R"(/containerPath)", false},
36 + {LR"(C:\host Path:/container Path:ro)", LR"(C:\host Path)", R"(/container Path)", true},
37 + {LR"(C:\host Path:/container Path:rw)", LR"(C:\host Path)", R"(/container Path)", false},
38 +
39 + // Relative paths. Expected host is CWD + the normalized relative component.
40 + // Windows will convert forward slashes to backslashes in the host path.
41 + {L"./foo:/data", (cwd / L"foo").wstring(), R"(/data)", false},
42 + {L".\\foo:/data", (cwd / L"foo").wstring(), R"(/data)", false},
43 + {L"./foo:/data:ro", (cwd / L"foo").wstring(), R"(/data)", true},
44 + {L"../bar:/data:rw", (cwd.parent_path() / L"bar").wstring(), R"(/data)", false},
45 + {L"..\\bar:/data:rw", (cwd.parent_path() / L"bar").wstring(), R"(/data)", false},
46 + {L"sub/dir:/data", (cwd / L"sub" / L"dir").wstring(), R"(/data)", false},
47 + {L"sub\\dir:/data", (cwd / L"sub" / L"dir").wstring(), R"(/data)", false},
48 + };
49 +
50 + for (const auto& arg : validVolumeArgs)
51 + {
52 + WEX::Logging::Log::Comment(std::format(L"Testing volume argument: '{}'", std::get<0>(arg)).c_str());
53 + auto result = VolumeMount::Parse(std::get<0>(arg));
54 + VERIFY_ARE_EQUAL(std::get<1>(arg), result.Host());
55 + VERIFY_ARE_EQUAL(std::get<2>(arg), result.ContainerPath());
56 + VERIFY_ARE_EQUAL(std::get<3>(arg), result.IsReadOnly());
57 + }
58 + }
59 +
60 + TEST_METHOD(VolumeMount_Parse_InvalidArgs)
61 + {
62 + std::vector<std::wstring> invalidCases = {
63 + LR"(:/containerPath)", // Empty host path
64 + LR"(:/containerPath:ro)", // Empty host path
65 + LR"(:)", // Empty container path
66 + LR"(::)", // Empty container path
67 + LR"(C:\hostPath::ro)", // Empty container path
68 + LR"(C:\hostPath:)", // Empty container path
69 + LR"(C:\hostPath::rw)", // Empty container path
70 + LR"(C:\hostPath:/containerPath:)", // Empty container path
71 + };
72 +
73 + for (const auto& value : invalidCases)
74 + {
75 + WEX::Logging::Log::Comment(std::format(L"Testing invalid volume argument: '{}'", value).c_str());
76 + VERIFY_THROWS(VolumeMount::Parse(value), wil::ResultException);
77 + }
78 + }
79 +
80 + TEST_METHOD(VolumeMount_Parse_InvalidContainerPath)
81 + {
82 + // Drive colon gets misinterpreted as the host:container separator,
83 + // producing a non-absolute container path. Caught by container path validation.
84 + std::vector<std::wstring> invalidPathCases = {
85 + LR"(C:\hostPath:ro)", // host='C', container=\hostPath (not absolute)
86 + LR"(C:\hostPath)", // host='C', container=\hostPath (not absolute)
87 + LR"(C:\hostPath:/containerPath:invalid_mode)", // container=invalid_mode (not absolute)
88 + LR"(C:\hostPath:/containerPath:ro:extra)", // container=extra (not absolute)
89 + };
90 +
91 + for (const auto& value : invalidPathCases)
92 + {
93 + WEX::Logging::Log::Comment(std::format(L"Testing invalid path: '{}'", value).c_str());
94 + VERIFY_THROWS(VolumeMount::Parse(value), wil::ResultException);
95 + }
96 + }
97 +
98 + TEST_METHOD(VolumeMount_IsValidNamedVolumeName_ValidNames)
99 + {
100 + // These should all be recognised as valid Docker named volume names.
101 + std::vector<std::wstring> validNames = {
102 + L"myvolume", // simple lowercase
103 + L"MyVolume", // mixed case
104 + L"my-volume", // hyphen
105 + L"my_volume", // underscore
106 + L"my.volume", // dot
107 + L"v1", // minimum length (2 chars)
108 + L"volume123", // trailing digits
109 + L"my-vol_1.0", // combination of all allowed characters
110 + };
111 +
112 + for (const auto& name : validNames)
113 + {
114 + WEX::Logging::Log::Comment(std::format(L"Testing valid named volume name: '{}'", name).c_str());
115 + VERIFY_IS_TRUE(VolumeMount::IsValidNamedVolumeName(name));
116 + }
117 + }
118 +
119 + TEST_METHOD(VolumeMount_IsValidNamedVolumeName_InvalidNames)
120 + {
121 + // These should all be rejected. They are either paths or otherwise invalid.
122 + std::vector<std::wstring> invalidNames = {
123 + L"./foo", // relative path with ./
124 + L"../foo", // relative path with ../
125 + L".hidden", // starts with '.' (relative path indicator)
126 + L"foo/bar", // contains forward slash (path separator)
127 + L"foo\\bar", // contains backslash (path separator)
128 + L"C:\\path\\to\\dir", // absolute Windows path
129 + L"/absolute/path", // absolute Unix-style path
130 + L"a", // too short (single character)
131 + L"", // empty
132 + L":volume", // starts with invalid character
133 + L"-volume", // starts with hyphen (not alphanumeric)
134 + L"_volume", // starts with underscore (not alphanumeric)
135 + L"vol ume", // contains space
136 + L"vol:ume", // contains colon
137 + };
138 +
139 + for (const auto& name : invalidNames)
140 + {
141 + WEX::Logging::Log::Comment(std::format(L"Testing invalid named volume name: '{}'", name).c_str());
142 + VERIFY_IS_FALSE(VolumeMount::IsValidNamedVolumeName(name));
143 + }
144 + }
145 +};
146 +} // namespace WSLCVolumeMount
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp new
+167
@@ -0,0 +1,167 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerAttachTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC container attach command.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +
21 +class WSLCE2EContainerAttachTests
22 +{
23 + WSLC_TEST_CLASS(WSLCE2EContainerAttachTests)
24 +
25 + TEST_CLASS_SETUP(ClassSetup)
26 + {
27 + EnsureImageIsLoaded(DebianImage);
28 + return true;
29 + }
30 +
31 + TEST_CLASS_CLEANUP(ClassCleanup)
32 + {
33 + EnsureContainerDoesNotExist(WslcContainerName);
34 + EnsureImageIsDeleted(DebianImage);
35 + return true;
36 + }
37 +
38 + TEST_METHOD_SETUP(TestMethodSetup)
39 + {
40 + EnsureContainerDoesNotExist(WslcContainerName);
41 + return true;
42 + }
43 +
44 + WSLC_TEST_METHOD(WSLCE2E_Container_Attach_HelpCommand)
45 + {
46 + auto result = RunWslc(L"container attach --help");
47 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
48 + }
49 +
50 + WSLC_TEST_METHOD(WSLCE2E_Container_Attach_TTY)
51 + {
52 + VerifyContainerIsNotListed(WslcContainerName);
53 +
54 + const auto& prompt = ">";
55 + auto result = RunWslc(std::format(
56 + L"container run -itd -e PS1={} --name {} {} bash --norc", prompt, WslcContainerName, DebianImage.NameAndTag()));
57 + result.Verify({.Stderr = L"", .ExitCode = 0});
58 + auto containerId = result.GetStdoutOneLine();
59 +
60 + const auto& expectedAttachPrompt = VT::BuildContainerAttachPrompt(prompt);
61 + const auto& expectedPrompt = VT::BuildContainerPrompt(prompt);
62 +
63 + auto session = RunWslcInteractive(std::format(L"container attach {}", containerId));
64 + VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
65 +
66 + // The container attach prompt appears twice.
67 + session.ExpectStdout(expectedAttachPrompt);
68 + session.ExpectStdout(expectedAttachPrompt);
69 +
70 + session.WriteLine("echo hello");
71 + session.ExpectCommandEcho("echo hello");
72 + session.ExpectStdout("hello\r\n");
73 + session.ExpectStdout(expectedPrompt);
74 +
75 + session.WriteLine("whoami");
76 + session.ExpectCommandEcho("whoami");
77 + session.ExpectStdout("root\r\n");
78 + session.ExpectStdout(expectedPrompt);
79 +
80 + session.ExitAndVerifyNoErrors();
81 + auto exitCode = session.Wait();
82 + VERIFY_ARE_EQUAL(0, exitCode);
83 + }
84 +
85 + WSLC_TEST_METHOD(WSLCE2E_Container_Attach_NoTTY)
86 + {
87 + VerifyContainerIsNotListed(WslcContainerName);
88 + auto result = RunWslc(std::format(L"container run -id --name {} {} cat", WslcContainerName, DebianImage.NameAndTag()));
89 + result.Verify({.Stderr = L"", .ExitCode = 0});
90 + auto containerId = result.GetStdoutOneLine();
91 +
92 + auto session = RunWslcInteractive(std::format(L"container attach {}", containerId));
93 + VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
94 +
95 + session.WriteLine("test line 1");
96 + session.ExpectStdout("test line 1\n");
97 + session.WriteLine("test line 2");
98 + session.ExpectStdout("test line 2\n");
99 +
100 + // Close stdin to signal EOF to cat
101 + session.CloseStdin();
102 +
103 + // Wait for cat to exit with code 0
104 + auto exitCode = session.Wait(10000);
105 + VERIFY_ARE_EQUAL(0, exitCode, L"Cat should exit with code 0 after receiving EOF");
106 + session.VerifyNoErrors();
107 + }
108 +
109 + WSLC_TEST_METHOD(WSLCE2E_Container_Attach_MissingContainerId)
110 + {
111 + auto result = RunWslc(L"container attach");
112 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
113 + }
114 +
115 + WSLC_TEST_METHOD(WSLCE2E_Container_Attach_ContainerNotFound)
116 + {
117 + auto result = RunWslc(std::format(L"container attach {}", WslcContainerName));
118 + result.Verify(
119 + {.Stderr = std::format(L"Container '{}' not found.\r\nError code: WSLC_E_CONTAINER_NOT_FOUND\r\n", WslcContainerName),
120 + .ExitCode = 1});
121 + }
122 +
123 +private:
124 + const std::wstring WslcContainerName = L"wslc-test-container";
125 + const TestImage& DebianImage = DebianTestImage();
126 +
127 + std::wstring GetHelpMessage() const
128 + {
129 + std::wstringstream output;
130 + output << GetWslcHeader() //
131 + << GetDescription() //
132 + << GetUsage() //
133 + << GetAvailableCommands() //
134 + << GetAvailableOptions();
135 + return output.str();
136 + }
137 +
138 + std::wstring GetDescription() const
139 + {
140 + return L"Attaches to a container.\r\n\r\n";
141 + }
142 +
143 + std::wstring GetUsage() const
144 + {
145 + return L"Usage: wslc container attach [<options>] <container-id>\r\n\r\n";
146 + }
147 +
148 + std::wstring GetAvailableCommands() const
149 + {
150 + std::wstringstream commands;
151 + commands << L"The following arguments are available:\r\n" //
152 + << L" container-id Container ID\r\n" //
153 + << L"\r\n";
154 + return commands.str();
155 + }
156 +
157 + std::wstring GetAvailableOptions() const
158 + {
159 + std::wstringstream options;
160 + options << L"The following options are available:\r\n" //
161 + << L" --session Specify the session to use\r\n" //
162 + << L" -?,--help Shows help about the selected command\r\n"
163 + << L"\r\n";
164 + return options.str();
165 + }
166 +};
167 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp new
+749
@@ -0,0 +1,749 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerCreateTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +#include <fstream>
19 +#include <wil/network.h>
20 +#include <wil/resource.h>
21 +
22 +namespace WSLCE2ETests {
23 +using namespace wsl::shared;
24 +
25 +using namespace WEX::Logging;
26 +
27 +class WSLCE2EContainerCreateTests
28 +{
29 + WSLC_TEST_CLASS(WSLCE2EContainerCreateTests)
30 +
31 + TEST_CLASS_SETUP(ClassSetup)
32 + {
33 + EnsureImageIsLoaded(AlpineImage);
34 + EnsureImageIsLoaded(DebianImage);
35 +
36 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), HostEnvVariableValue.c_str()));
37 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), HostEnvVariableValue2.c_str()));
38 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr));
39 + return true;
40 + }
41 +
42 + TEST_CLASS_CLEANUP(ClassCleanup)
43 + {
44 + EnsureContainerDoesNotExist(WslcContainerName);
45 + EnsureImageIsDeleted(AlpineImage);
46 + EnsureImageIsDeleted(DebianImage);
47 +
48 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
49 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr));
50 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr));
51 + return true;
52 + }
53 +
54 + TEST_METHOD_SETUP(TestMethodSetup)
55 + {
56 + EnvTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
57 + EnvTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
58 + VolumeTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
59 + VolumeTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
60 + EnsureContainerDoesNotExist(WslcContainerName);
61 + return true;
62 + }
63 +
64 + TEST_METHOD_CLEANUP(TestMethodCleanup)
65 + {
66 + DeleteFileW(EnvTestFile1.c_str());
67 + DeleteFileW(EnvTestFile2.c_str());
68 + DeleteFileW(VolumeTestFile1.c_str());
69 + DeleteFileW(VolumeTestFile2.c_str());
70 + return true;
71 + }
72 +
73 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_HelpCommand)
74 + {
75 + auto result = RunWslc(L"container create --help");
76 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
77 + }
78 +
79 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_MissingImage)
80 + {
81 + auto result = RunWslc(L"container create --name " + WslcContainerName);
82 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
83 + }
84 +
85 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_InvalidImage)
86 + {
87 + auto result = RunWslc(L"container create --name " + WslcContainerName + L" " + InvalidImage.NameAndTag());
88 + std::wstringstream expectedError;
89 + expectedError << L"Image '" << InvalidImage.NameAndTag() << L"' not found, pulling\r\n"
90 + << L"manifest for " << InvalidImage.NameAndTag()
91 + << L" not found: manifest unknown: manifest tagged by \"latest\" is not found\r\n"
92 + << L"Error code: WSLC_E_IMAGE_NOT_FOUND\r\n";
93 + result.Verify({.Stderr = expectedError.str(), .ExitCode = 1});
94 + }
95 +
96 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Valid)
97 + {
98 + VerifyContainerIsNotListed(WslcContainerName);
99 +
100 + // Create the container with a valid image
101 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
102 + result.Verify({.Stderr = L"", .ExitCode = 0});
103 + std::wstring containerId = result.GetStdoutOneLine();
104 +
105 + // Verify the container is listed with the correct status
106 + VerifyContainerIsListed(containerId, L"created");
107 + }
108 +
109 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_DuplicateContainerName)
110 + {
111 + VerifyContainerIsNotListed(WslcContainerName);
112 +
113 + // Create the container with a valid image
114 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
115 + result.Verify({.Stderr = L"", .ExitCode = 0});
116 + auto containerId = result.GetStdoutOneLine();
117 +
118 + // Attempt to create another container with the same name
119 + result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
120 + result.Verify(
121 + {.Stderr = std::format(L"Conflict. The container name \"/{}\" is already in use by container \"{}\". You have to remove (or rename) that container to be able to reuse that name.\r\nError code: ERROR_ALREADY_EXISTS\r\n", WslcContainerName, containerId),
122 + .ExitCode = 1});
123 + }
124 +
125 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_WriteFromHostReadFromContainer)
126 + {
127 + // Write to a temp file that we will mount as a volume to the container
128 + const std::wstring tempFile = VolumeTestFile1.wstring();
129 + std::wofstream out(tempFile);
130 + out << L"WSLC Volume Test";
131 + out.close();
132 +
133 + auto hostDirectory = VolumeTestFile1.parent_path();
134 + auto fileName = VolumeTestFile1.filename().wstring();
135 +
136 + auto result = RunWslc(std::format(
137 + L"container run --name {} --volume \"{}:/data:ro\" {} cat /data/{}",
138 + WslcContainerName,
139 + hostDirectory.wstring(),
140 + AlpineImage.NameAndTag(),
141 + fileName));
142 + result.Verify({.Stdout = L"WSLC Volume Test", .Stderr = L"", .ExitCode = 0});
143 + }
144 +
145 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_WriteFromContainerReadFromHost_ReadWritePermissionByDefault)
146 + {
147 + auto hostDirectory = VolumeTestFile1.parent_path();
148 + auto fileName = VolumeTestFile1.filename().wstring();
149 + auto result = RunWslc(std::format(
150 + L"container run --name {} --volume \"{}:/data\" {} sh -c \"echo -n 'WSLC Volume Test' > /data/{}\"",
151 + WslcContainerName,
152 + hostDirectory.wstring(),
153 + AlpineImage.NameAndTag(),
154 + fileName));
155 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
156 +
157 + // Read all file content
158 + auto content = ReadFileContent(VolumeTestFile1.wstring());
159 + VERIFY_ARE_EQUAL(L"WSLC Volume Test", content);
160 + }
161 +
162 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_WriteFromContainerReadFromHost_ReadWritePermission)
163 + {
164 + auto hostDirectory = VolumeTestFile1.parent_path();
165 + auto fileName = VolumeTestFile1.filename().wstring();
166 + auto result = RunWslc(std::format(
167 + L"container run --name {} --volume \"{}:/data:rw\" {} sh -c \"echo -n 'WSLC Volume Test' > /data/{}\"",
168 + WslcContainerName,
169 + hostDirectory.wstring(),
170 + AlpineImage.NameAndTag(),
171 + fileName));
172 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
173 +
174 + // Read all file content
175 + std::wifstream in(VolumeTestFile1);
176 + std::wstringstream buffer;
177 + buffer << in.rdbuf();
178 + VERIFY_ARE_EQUAL(L"WSLC Volume Test", buffer.str());
179 + }
180 +
181 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_WriteFromContainerReadFromHost_ReadOnlyPermission_Fail)
182 + {
183 + auto hostDirectory = VolumeTestFile1.parent_path();
184 + auto fileName = VolumeTestFile1.filename().wstring();
185 + auto result = RunWslc(std::format(
186 + L"container run --name {} --volume \"{}:/data:ro\" {} sh -c \"echo -n 'WSLC Volume Test' > /data/{}\"",
187 + WslcContainerName,
188 + hostDirectory.wstring(),
189 + AlpineImage.NameAndTag(),
190 + fileName));
191 + auto errorMessage = std::format(L"sh: can't create /data/{}: Read-only file system\n", fileName);
192 + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
193 + }
194 +
195 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_Multiple_WriteFromContainerReadFromHost_ReadWritePermission)
196 + {
197 + // Mount multiple volumes to the container
198 + auto hostDirectory1 = VolumeTestFile1.parent_path();
199 + auto fileName1 = VolumeTestFile1.filename().wstring();
200 + auto hostDirectory2 = VolumeTestFile2.parent_path();
201 + auto fileName2 = VolumeTestFile2.filename().wstring();
202 + auto result = RunWslc(std::format(
203 + L"container run --name {} --volume \"{}:/data1:rw\" --volume \"{}:/data2:rw\" {} sh -c \"echo -n 'Test1' > "
204 + L"/data1/{} && "
205 + L"echo -n 'Test2' > /data2/{}\"",
206 + WslcContainerName,
207 + hostDirectory1.wstring(),
208 + hostDirectory2.wstring(),
209 + AlpineImage.NameAndTag(),
210 + fileName1,
211 + fileName2));
212 +
213 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
214 +
215 + // Read all file content for both files
216 + std::wifstream in1(VolumeTestFile1);
217 + std::wstringstream buffer1;
218 + buffer1 << in1.rdbuf();
219 + VERIFY_ARE_EQUAL(L"Test1", buffer1.str());
220 +
221 + std::wifstream in2(VolumeTestFile2);
222 + std::wstringstream buffer2;
223 + buffer2 << in2.rdbuf();
224 + VERIFY_ARE_EQUAL(L"Test2", buffer2.str());
225 + }
226 +
227 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_RelativeHostPath)
228 + {
229 + // Create a uniquely-named subdirectory relative to the CWD and pass it as a relative path
230 + // to wslc. Using a GUID suffix avoids collisions on parallel runs or after a prior crash.
231 + // Uses "./" prefix to disambiguate from a Docker named volume name.
232 + GUID runId;
233 + THROW_IF_FAILED(CoCreateGuid(&runId));
234 + const auto dirName =
235 + L"wslc-vol-relpath-" + wsl::shared::string::GuidToString<wchar_t>(runId, wsl::shared::string::GuidToStringFlags::None);
236 + const auto absoluteDir = std::filesystem::current_path() / dirName;
237 + std::filesystem::create_directories(absoluteDir);
238 + auto cleanupDir = wil::scope_exit([&]() { std::filesystem::remove_all(absoluteDir); });
239 +
240 + const auto testFile = absoluteDir / L"reltest.txt";
241 + const auto relativeDir = L"./" + dirName;
242 +
243 + // Write a file from the host and verify the container can read it via the relative path mount.
244 + {
245 + std::ofstream out(testFile);
246 + VERIFY_IS_TRUE(out.is_open(), L"Failed to open test file for writing (host -> container test)");
247 + out << "WSLC Relative Path Test";
248 + VERIFY_IS_TRUE(out.good(), L"Failed to write to test file (host -> container test)");
249 + }
250 +
251 + auto result = RunWslc(std::format(
252 + L"container run --rm --name {} --volume \"{}:/data:ro\" {} cat /data/reltest.txt",
253 + WslcContainerName,
254 + relativeDir,
255 + AlpineImage.NameAndTag()));
256 + result.Verify({.Stdout = L"WSLC Relative Path Test", .Stderr = L"", .ExitCode = 0});
257 +
258 + EnsureContainerDoesNotExist(WslcContainerName);
259 +
260 + // Write a file from the container and verify the host can read it back via the relative path mount.
261 + result = RunWslc(std::format(
262 + L"container run --rm --name {} --volume \"{}:/data:rw\" {} sh -c \"echo -n 'WSLC Relative Path Write Test' > "
263 + L"/data/reltest.txt\"",
264 + WslcContainerName,
265 + relativeDir,
266 + AlpineImage.NameAndTag()));
267 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
268 +
269 + std::ifstream in(testFile);
270 + VERIFY_IS_TRUE(in.is_open(), L"Failed to open test file for reading (container -> host test)");
271 + std::stringstream buffer;
272 + buffer << in.rdbuf();
273 + VERIFY_IS_TRUE(in.good() || in.eof(), L"Failed to read test file (container -> host test)");
274 + VERIFY_ARE_EQUAL("WSLC Relative Path Write Test", buffer.str());
275 + }
276 +
277 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_Invalid)
278 + {
279 + {
280 + auto result =
281 + RunWslc(std::format(L"container run --name {} --volume :/containerPath {}", WslcContainerName, AlpineImage.NameAndTag()));
282 + result.Verify({.Stderr = L"Invalid volume specifications: ':/containerPath'. Host path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
283 + EnsureContainerDoesNotExist(WslcContainerName);
284 + }
285 +
286 + {
287 + auto result = RunWslc(
288 + std::format(L"container run --name {} --volume C:\\hostPath::ro {}", WslcContainerName, AlpineImage.NameAndTag()));
289 + result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath::ro'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
290 + EnsureContainerDoesNotExist(WslcContainerName);
291 + }
292 +
293 + {
294 + auto result = RunWslc(
295 + std::format(L"container run --name {} --volume :/containerPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
296 + result.Verify({.Stderr = L"Invalid volume specifications: ':/containerPath:ro'. Host path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
297 + EnsureContainerDoesNotExist(WslcContainerName);
298 + }
299 +
300 + {
301 + auto result = RunWslc(std::format(L"container run --name {} --volume \"\" {}", WslcContainerName, AlpineImage.NameAndTag()));
302 + result.Verify({.Stderr = L"Invalid volume specifications: ''. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
303 + EnsureContainerDoesNotExist(WslcContainerName);
304 + }
305 +
306 + {
307 + auto result =
308 + RunWslc(std::format(L"container run --name {} --volume C:\\hostPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
309 + result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
310 + EnsureContainerDoesNotExist(WslcContainerName);
311 + }
312 +
313 + {
314 + auto result =
315 + RunWslc(std::format(L"container run --name {} --volume C:\\hostPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
316 + result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:ro'. Container path must be an absolute path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
317 + EnsureContainerDoesNotExist(WslcContainerName);
318 + }
319 +
320 + {
321 + auto result = RunWslc(std::format(L"container run --name {} --volume :ro {}", WslcContainerName, AlpineImage.NameAndTag()));
322 + result.Verify({.Stderr = L"Invalid volume specifications: ':ro'. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
323 + EnsureContainerDoesNotExist(WslcContainerName);
324 + }
325 +
326 + {
327 + auto result = RunWslc(
328 + std::format(L"container run --name {} --volume C:\\hostPath::rw {}", WslcContainerName, AlpineImage.NameAndTag()));
329 + result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath::rw'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
330 + EnsureContainerDoesNotExist(WslcContainerName);
331 + }
332 +
333 + {
334 + auto result = RunWslc(std::format(
335 + L"container run --name {} --volume C:\\hostPath:/containerPath:invalid_mode {}", WslcContainerName, AlpineImage.NameAndTag()));
336 + result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:/containerPath:invalid_mode'. Container path must be an absolute path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
337 + EnsureContainerDoesNotExist(WslcContainerName);
338 + }
339 +
340 + {
341 + auto result = RunWslc(std::format(
342 + L"container run --name {} --volume C:\\hostPath:/containerPath:ro:extra {}", WslcContainerName, AlpineImage.NameAndTag()));
343 + result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:/containerPath:ro:extra'. Container path must be an absolute path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
344 + EnsureContainerDoesNotExist(WslcContainerName);
345 + }
346 +
347 + {
348 + auto result = RunWslc(std::format(
349 + L"container run --name {} --volume C:\\hostPath:/containerPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
350 + result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:/containerPath:'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
351 + EnsureContainerDoesNotExist(WslcContainerName);
352 + }
353 +
354 + {
355 + // "::/container:ro" - host=":", container="/container". ":" is not a valid Windows path.
356 + auto result = RunWslc(
357 + std::format(L"container run --name {} --volume \"::/container:ro\" {}", WslcContainerName, AlpineImage.NameAndTag()));
358 + result.Verify({.Stderr = L"Invalid volume specifications: '::/container:ro'. Host path ':' is not a valid Windows path.\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
359 + EnsureContainerDoesNotExist(WslcContainerName);
360 + }
361 + }
362 +
363 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_NotSupported)
364 + {
365 + // Commands tested in this method are not currently supported in WSLC,
366 + // so we just verify that they fail with the expected error message.
367 + // https://github.com/microsoft/WSL/issues/14432
368 + {
369 + auto result = RunWslc(
370 + std::format(L"container run --name {} --volume \"C:\\hostPath\" {}", WslcContainerName, AlpineImage.NameAndTag()));
371 + result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath'. Container path must be an absolute path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
372 + EnsureContainerDoesNotExist(WslcContainerName);
373 + }
374 +
375 + {
376 + auto result = RunWslc(std::format(L"container run --name {} --volume \":\" {}", WslcContainerName, AlpineImage.NameAndTag()));
377 + result.Verify({.Stderr = L"Invalid volume specifications: ':'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
378 + EnsureContainerDoesNotExist(WslcContainerName);
379 + }
380 +
381 + {
382 + // "::" splits as host=":", container="". Container path empty check fires first.
383 + auto result =
384 + RunWslc(std::format(L"container run --name {} --volume \"::\" {}", WslcContainerName, AlpineImage.NameAndTag()));
385 + result.Verify({.Stderr = L"Invalid volume specifications: '::'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
386 + EnsureContainerDoesNotExist(WslcContainerName);
387 + }
388 +
389 + {
390 + auto result =
391 + RunWslc(std::format(L"container run --name {} --volume \"e2e_test\" {}", WslcContainerName, AlpineImage.NameAndTag()));
392 + result.Verify({.Stderr = L"Invalid volume specifications: 'e2e_test'. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
393 + EnsureContainerDoesNotExist(WslcContainerName);
394 + }
395 + }
396 +
397 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Remove)
398 + {
399 + VerifyContainerIsNotListed(WslcContainerName);
400 +
401 + auto result = RunWslc(std::format(L"container create --rm --name {} {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
402 + result.Verify({.Stderr = L"", .ExitCode = 0});
403 +
404 + // Start the container.
405 + result = RunWslc(std::format(L"container start {}", WslcContainerName));
406 + result.Verify({.Stderr = L"", .ExitCode = 0});
407 +
408 + // Verify with retry timeout of 1 minute.
409 + VerifyContainerIsNotListed(WslcContainerName, std::chrono::seconds(2), std::chrono::minutes(1));
410 + }
411 +
412 + WSLC_TEST_METHOD(WSLCE2E_Container_Start_AlreadyRunning)
413 + {
414 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
415 + result.Verify({.Stderr = L"", .ExitCode = 0});
416 +
417 + auto containerId = result.GetStdoutOneLine();
418 + VERIFY_IS_FALSE(containerId.empty());
419 +
420 + VerifyContainerIsListed(containerId, L"running");
421 +
422 + // Start again - should succeed without error
423 + result = RunWslc(std::format(L"container start {}", WslcContainerName));
424 + result.Verify({.Stderr = L"", .ExitCode = 0});
425 +
426 + // Verify the container is still running
427 + VerifyContainerIsListed(containerId, L"running");
428 + }
429 +
430 + WSLC_TEST_METHOD(WSLCE2E_Container_CreateStartAttach_TTY)
431 + {
432 + VerifyContainerIsNotListed(WslcContainerName);
433 +
434 + const auto& prompt = ">";
435 + auto result = RunWslc(std::format(
436 + L"container create -it -e PS1={} --name {} {} bash --norc", prompt, WslcContainerName, DebianImage.NameAndTag()));
437 + result.Verify({.Stderr = L"", .ExitCode = 0});
438 + auto containerId = result.GetStdoutOneLine();
439 +
440 + const auto& expectedPrompt = VT::BuildContainerPrompt(prompt);
441 +
442 + auto session = RunWslcInteractive(std::format(L"container start --attach {}", containerId));
443 + VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
444 +
445 + session.ExpectStdout(expectedPrompt);
446 +
447 + session.WriteLine("echo hello");
448 + session.ExpectCommandEcho("echo hello");
449 + session.ExpectStdout("hello\r\n");
450 + session.ExpectStdout(expectedPrompt);
451 +
452 + session.WriteLine("whoami");
453 + session.ExpectCommandEcho("whoami");
454 + session.ExpectStdout("root\r\n");
455 + session.ExpectStdout(expectedPrompt);
456 +
457 + session.ExitAndVerifyNoErrors();
458 + auto exitCode = session.Wait();
459 + VERIFY_ARE_EQUAL(0, exitCode);
460 + }
461 +
462 + WSLC_TEST_METHOD(WSLCE2E_Container_CreateStartAttach_NoTTY)
463 + {
464 + VerifyContainerIsNotListed(WslcContainerName);
465 + auto result = RunWslc(std::format(L"container create -i --name {} {} cat", WslcContainerName, DebianImage.NameAndTag()));
466 + result.Verify({.Stderr = L"", .ExitCode = 0});
467 + auto containerId = result.GetStdoutOneLine();
468 +
469 + // Start with attach
470 + auto session = RunWslcInteractive(std::format(L"container start --attach {}", containerId));
471 + VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
472 +
473 + session.WriteLine("test line 1");
474 + session.ExpectStdout("test line 1\n");
475 + session.WriteLine("test line 2");
476 + session.ExpectStdout("test line 2\n");
477 +
478 + // Close stdin to signal EOF to cat
479 + session.CloseStdin();
480 +
481 + // Wait for cat to exit with code 0
482 + auto exitCode = session.Wait(10000);
483 + VERIFY_ARE_EQUAL(0, exitCode, L"Cat should exit with code 0 after receiving EOF");
484 + session.VerifyNoErrors();
485 + }
486 +
487 + WSLC_TEST_METHOD(WSLCE2E_Container_CreateStartAttach_ShortRunningInitProcess)
488 + {
489 + VerifyContainerIsNotListed(WslcContainerName);
490 +
491 + constexpr auto ExpectedExitCode = 37;
492 +
493 + auto result = RunWslc(std::format(
494 + L"container create --name {} {} sh -c \"echo lifecycle works; exit {}\"", WslcContainerName, AlpineImage.NameAndTag(), ExpectedExitCode));
495 +
496 + result.Verify({.Stderr = L"", .ExitCode = 0});
497 +
498 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
499 + result.Verify({.Stdout = L"lifecycle works\n", .Stderr = L"", .ExitCode = ExpectedExitCode});
500 + }
501 +
502 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_UserOption_UidRoot)
503 + {
504 + auto result = RunWslc(
505 + std::format(L"container create --name {} -u 0 {} sh -c \"id -u; id -g\"", WslcContainerName, DebianImage.NameAndTag()));
506 + result.Verify({.Stderr = L"", .ExitCode = 0});
507 +
508 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
509 + result.Verify({.Stdout = L"0\n0\n", .Stderr = L"", .ExitCode = 0});
510 + }
511 +
512 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_UserOption_NameGroupRoot)
513 + {
514 + auto result = RunWslc(std::format(
515 + L"container create --name {} -u root:root {} sh -c \"id -un; id -u; id -g\"", WslcContainerName, DebianImage.NameAndTag()));
516 + result.Verify({.Stderr = L"", .ExitCode = 0});
517 +
518 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
519 + result.Verify({.Stdout = L"root\n0\n0\n", .Stderr = L"", .ExitCode = 0});
520 + }
521 +
522 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_UserOption_UnknownUser_Fails)
523 + {
524 + auto result = RunWslc(
525 + std::format(L"container create --name {} -u user_does_not_exist {} id -u", WslcContainerName, DebianImage.NameAndTag()));
526 + result.Verify({.Stderr = L"", .ExitCode = 0});
527 +
528 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
529 + result.Verify(
530 + {.Stderr = L"unable to find user user_does_not_exist: no matching entries in passwd file\r\nError code: E_FAIL\r\n", .ExitCode = 1});
531 + }
532 +
533 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs)
534 + {
535 + auto result = RunWslc(std::format(
536 + L"container create --name {} --tmpfs /wslc-tmpfs {} sh -c \"echo -n 'tmpfs_test' > /wslc-tmpfs/data && cat "
537 + L"/wslc-tmpfs/data\"",
538 + WslcContainerName,
539 + DebianImage.NameAndTag()));
540 + result.Verify({.Stderr = L"", .ExitCode = 0});
541 +
542 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
543 + result.Verify({.Stdout = L"tmpfs_test", .Stderr = L"", .ExitCode = 0});
544 + }
545 +
546 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_With_Options)
547 + {
548 + auto result = RunWslc(std::format(
549 + L"container create --name {} --tmpfs /wslc-tmpfs:size=64k {} sh -c \"mount | grep -q ' on /wslc-tmpfs type tmpfs ' "
550 + L"&& echo mounted\"",
551 + WslcContainerName,
552 + DebianImage.NameAndTag()));
553 + result.Verify({.Stderr = L"", .ExitCode = 0});
554 +
555 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
556 + result.Verify({.Stdout = L"mounted\n", .Stderr = L"", .ExitCode = 0});
557 + }
558 +
559 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_Multiple_With_Options)
560 + {
561 + auto result = RunWslc(std::format(
562 + L"container create --name {} --tmpfs /wslc-tmpfs1:size=64k --tmpfs /wslc-tmpfs2:size=128k {} sh -c \"mount | grep -q "
563 + L"' on /wslc-tmpfs1 type tmpfs ' && mount | grep -q ' on /wslc-tmpfs2 type tmpfs ' && echo mounted\"",
564 + WslcContainerName,
565 + DebianImage.NameAndTag()));
566 + result.Verify({.Stderr = L"", .ExitCode = 0});
567 +
568 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
569 + result.Verify({.Stdout = L"mounted\n", .Stderr = L"", .ExitCode = 0});
570 + }
571 +
572 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_RelativePath_Fails)
573 + {
574 + auto result =
575 + RunWslc(std::format(L"container create --name {} --tmpfs wslc-tmpfs {}", WslcContainerName, DebianImage.NameAndTag()));
576 + result.Verify({.Stderr = L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
577 + }
578 +
579 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_EmptyDestination_Fails)
580 + {
581 + auto result =
582 + RunWslc(std::format(L"container create --name {} --tmpfs :size=64k {}", WslcContainerName, DebianImage.NameAndTag()));
583 + result.Verify({.Stderr = L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
584 + }
585 +
586 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_WorkDir)
587 + {
588 + auto result =
589 + RunWslc(std::format(L"container create --name {} --workdir /tmp {} pwd", WslcContainerName, DebianImage.NameAndTag()));
590 + result.Verify({.Stderr = L"", .ExitCode = 0});
591 +
592 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
593 + result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
594 + }
595 +
596 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_WithLabel_Success)
597 + {
598 + auto result =
599 + RunWslc(std::format(L"container create --name {} --label A=1 --label B=2 {}", WslcContainerName, DebianImage.NameAndTag()));
600 + result.Verify({.Stderr = L"", .ExitCode = 0});
601 +
602 + auto inspect = InspectContainer(WslcContainerName);
603 + VERIFY_ARE_EQUAL("1", inspect.Labels["A"]);
604 + VERIFY_ARE_EQUAL("2", inspect.Labels["B"]);
605 + }
606 +
607 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Hostname)
608 + {
609 + auto result = RunWslc(std::format(
610 + L"container create --name {} --hostname my-test-host {} hostname", WslcContainerName, DebianImage.NameAndTag()));
611 + result.Verify({.Stderr = L"", .ExitCode = 0});
612 +
613 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
614 + result.Verify({.Stdout = L"my-test-host\n", .Stderr = L"", .ExitCode = 0});
615 + }
616 +
617 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Domainname)
618 + {
619 + auto result = RunWslc(std::format(
620 + L"container create --name {} --domainname my-test-domain {} dnsdomainname", WslcContainerName, DebianImage.NameAndTag()));
621 + result.Verify({.Stderr = L"", .ExitCode = 0});
622 +
623 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
624 + result.Verify({.Stdout = L"my-test-domain\n", .Stderr = L"", .ExitCode = 0});
625 + }
626 +
627 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_DNS)
628 + {
629 + auto result = RunWslc(std::format(
630 + L"container create --name {} --dns 1.1.1.1 --dns 8.8.8.8 {} cat /etc/resolv.conf", WslcContainerName, DebianImage.NameAndTag()));
631 + result.Verify({.Stderr = L"", .ExitCode = 0});
632 +
633 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
634 + result.Verify({.Stderr = L"", .ExitCode = 0});
635 + VERIFY_IS_TRUE(result.Stdout->find(L"nameserver 1.1.1.1") != std::wstring::npos);
636 + VERIFY_IS_TRUE(result.Stdout->find(L"nameserver 8.8.8.8") != std::wstring::npos);
637 + }
638 +
639 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_DNSSearch)
640 + {
641 + auto result = RunWslc(std::format(
642 + L"container create --name {} --dns-search example.com --dns-search test.local {} cat /etc/resolv.conf",
643 + WslcContainerName,
644 + DebianImage.NameAndTag()));
645 + result.Verify({.Stderr = L"", .ExitCode = 0});
646 +
647 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
648 + result.Verify({.Stderr = L"", .ExitCode = 0});
649 + VERIFY_IS_TRUE(result.Stdout->find(L"search example.com test.local") != std::wstring::npos);
650 + }
651 +
652 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_DNSOption)
653 + {
654 + auto result = RunWslc(std::format(
655 + L"container create --name {} --dns-option ndots:5 --dns-option timeout:3 {} cat /etc/resolv.conf",
656 + WslcContainerName,
657 + DebianImage.NameAndTag()));
658 + result.Verify({.Stderr = L"", .ExitCode = 0});
659 +
660 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
661 + result.Verify({.Stderr = L"", .ExitCode = 0});
662 + VERIFY_IS_TRUE(result.Stdout->find(L"options ndots:5 timeout:3") != std::wstring::npos);
663 + }
664 +
665 +private:
666 + // Test container name
667 + const std::wstring WslcContainerName = L"wslc-test-container";
668 +
669 + // Test environment variables
670 + const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
671 + const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
672 + const std::wstring HostEnvVariableValue = L"wslc-host-env-value";
673 + const std::wstring HostEnvVariableValue2 = L"wslc-host-env-value2";
674 + const std::wstring MissingHostEnvVariableName = L"WSLC_TEST_MISSING_HOST_ENV";
675 +
676 + // Test environment variable files
677 + std::filesystem::path EnvTestFile1;
678 + std::filesystem::path EnvTestFile2;
679 +
680 + // Test images
681 + const TestImage& AlpineImage = AlpineTestImage();
682 + const TestImage& DebianImage = DebianTestImage();
683 + const TestImage& InvalidImage = InvalidTestImage();
684 +
685 + // Test volume files
686 + std::filesystem::path VolumeTestFile1;
687 + std::filesystem::path VolumeTestFile2;
688 +
689 + std::wstring GetHelpMessage() const
690 + {
691 + std::wstringstream output;
692 + output << GetWslcHeader() //
693 + << GetDescription() //
694 + << GetUsage() //
695 + << GetAvailableCommands() //
696 + << GetAvailableOptions();
697 + return output.str();
698 + }
699 +
700 + std::wstring GetDescription() const
701 + {
702 + return Localization::WSLCCLI_ContainerCreateLongDesc() + L"\r\n\r\n";
703 + }
704 +
705 + std::wstring GetUsage() const
706 + {
707 + return L"Usage: wslc container create [<options>] <image> [<command>] [<arguments>...]\r\n\r\n";
708 + }
709 +
710 + std::wstring GetAvailableCommands() const
711 + {
712 + std::wstringstream commands;
713 + commands << L"The following arguments are available:\r\n"
714 + << L" image Image name\r\n"
715 + << L" command The command to run\r\n"
716 + << L" arguments Arguments to pass to container's init process\r\n\r\n";
717 + return commands.str();
718 + }
719 +
720 + std::wstring GetAvailableOptions() const
721 + {
722 + std::wstringstream options;
723 + options << L"The following options are available:\r\n" //
724 + << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
725 + << L" --dns-option Set DNS options\r\n"
726 + << L" --dns-search Set DNS search domains\r\n"
727 + << L" --domainname Container domain name\r\n"
728 + << L" --entrypoint Specifies the container init process executable\r\n"
729 + << L" -e,--env Key=Value pairs for environment variables\r\n"
730 + << L" --env-file File containing key=value pairs of env variables\r\n"
731 + << L" -h,--hostname Container host name\r\n"
732 + << L" -i,--interactive Attach to stdin and keep it open\r\n"
733 + << L" -l,--label Set metadata on an object\r\n"
734 + << L" --name Name of the container\r\n"
735 + << L" -p,--publish Publish a port from a container to host\r\n"
736 + << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
737 + << L" --rm Remove the container after it stops\r\n"
738 + << L" --session Specify the session to use\r\n"
739 + << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
740 + << L" -t,--tty Open a TTY with the container process.\r\n"
741 + << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
742 + << L" -v,--volume Bind mount a volume to the container\r\n"
743 + << L" -w,--workdir Working directory inside the container\r\n"
744 + << L" -?,--help Shows help about the selected command\r\n"
745 + << L"\r\n";
746 + return options.str();
747 + }
748 +};
749 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp new
+462
@@ -0,0 +1,462 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerExecTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC container exec command.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +
21 +class WSLCE2EContainerExecTests
22 +{
23 + WSLC_TEST_CLASS(WSLCE2EContainerExecTests)
24 +
25 + TEST_CLASS_SETUP(ClassSetup)
26 + {
27 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), HostEnvVariableValue.c_str()));
28 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), HostEnvVariableValue2.c_str()));
29 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr));
30 +
31 + EnsureImageIsLoaded(DebianImage);
32 + return true;
33 + }
34 +
35 + TEST_CLASS_CLEANUP(ClassCleanup)
36 + {
37 + EnsureContainerDoesNotExist(WslcContainerName);
38 + EnsureImageIsDeleted(DebianImage);
39 +
40 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
41 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr));
42 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr));
43 + return true;
44 + }
45 +
46 + TEST_METHOD_SETUP(TestMethodSetup)
47 + {
48 + EnvTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
49 + EnvTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
50 + EnsureContainerDoesNotExist(WslcContainerName);
51 + return true;
52 + }
53 +
54 + TEST_METHOD_CLEANUP(TestMethodCleanup)
55 + {
56 + DeleteFileW(EnvTestFile1.c_str());
57 + DeleteFileW(EnvTestFile2.c_str());
58 + return true;
59 + }
60 +
61 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_HelpCommand)
62 + {
63 + auto result = RunWslc(L"container exec --help");
64 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
65 + }
66 +
67 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_MissingContainerId)
68 + {
69 + auto result = RunWslc(L"container exec");
70 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
71 + }
72 +
73 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_MissingCommand)
74 + {
75 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
76 + result.Verify({.Stderr = L"", .ExitCode = 0});
77 +
78 + result = RunWslc(std::format(L"container exec {}", WslcContainerName));
79 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'command'\r\n", .ExitCode = 1});
80 + }
81 +
82 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_ContainerNotFound)
83 + {
84 + auto result = RunWslc(std::format(L"container exec {} echo hello", WslcContainerName));
85 + result.Verify(
86 + {.Stderr = std::format(L"Container '{}' not found.\r\nError code: WSLC_E_CONTAINER_NOT_FOUND\r\n", WslcContainerName),
87 + .ExitCode = 1});
88 + }
89 +
90 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_SimpleCommand)
91 + {
92 + // Run a container in background
93 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
94 + result.Verify({.Stderr = L"", .ExitCode = 0});
95 +
96 + // Execute a command
97 + result = RunWslc(std::format(L"container exec {} echo hello", WslcContainerName));
98 + result.Verify({.Stdout = L"hello\n", .Stderr = L"", .ExitCode = 0});
99 + }
100 +
101 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_InteractiveTTY)
102 + {
103 + VerifyContainerIsNotListed(WslcContainerName);
104 +
105 + const auto& prompt = ">";
106 + auto result =
107 + RunWslc(std::format(L"container run -itd -e PS1={} --name {} {}", prompt, WslcContainerName, DebianImage.NameAndTag()));
108 + result.Verify({.Stderr = L"", .ExitCode = 0});
109 + auto containerId = result.GetStdoutOneLine();
110 +
111 + const auto& expectedPrompt = VT::BuildContainerPrompt(prompt);
112 +
113 + auto session = RunWslcInteractive(std::format(L"container exec -it {} /bin/bash --norc", containerId));
114 + VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
115 +
116 + session.ExpectStdout(expectedPrompt);
117 +
118 + session.WriteLine("echo hello");
119 + session.ExpectCommandEcho("echo hello");
120 + session.ExpectStdout("hello\r\n");
121 + session.ExpectStdout(expectedPrompt);
122 +
123 + session.WriteLine("whoami");
124 + session.ExpectCommandEcho("whoami");
125 + session.ExpectStdout("root\r\n");
126 + session.ExpectStdout(expectedPrompt);
127 +
128 + session.ExitAndVerifyNoErrors();
129 + auto exitCode = session.Wait();
130 + VERIFY_ARE_EQUAL(0, exitCode);
131 + }
132 +
133 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_InteractiveNoTTY)
134 + {
135 + VerifyContainerIsNotListed(WslcContainerName);
136 + auto result = RunWslc(std::format(L"container run -id --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
137 + result.Verify({.Stderr = L"", .ExitCode = 0});
138 + auto containerId = result.GetStdoutOneLine();
139 +
140 + auto session = RunWslcInteractive(std::format(L"container exec -i {} cat", containerId));
141 + VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
142 +
143 + session.WriteLine("test line 1");
144 + session.ExpectStdout("test line 1\n");
145 + session.WriteLine("test line 2");
146 + session.ExpectStdout("test line 2\n");
147 +
148 + // Close stdin to signal EOF to cat
149 + session.CloseStdin();
150 +
151 + // Wait for cat to exit with code 0
152 + auto exitCode = session.Wait(10000);
153 + VERIFY_ARE_EQUAL(0, exitCode, L"Cat should exit with code 0 after receiving EOF");
154 + session.VerifyNoErrors();
155 + }
156 +
157 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption)
158 + {
159 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
160 + result.Verify({.Stderr = L"", .ExitCode = 0});
161 +
162 + result = RunWslc(std::format(L"container exec -e {}=A {} env", HostEnvVariableName, WslcContainerName));
163 + result.Verify({.Stderr = L"", .ExitCode = 0});
164 +
165 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=A", HostEnvVariableName)));
166 + }
167 +
168 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_KeyOnly_UsesHostValue)
169 + {
170 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
171 + result.Verify({.Stderr = L"", .ExitCode = 0});
172 +
173 + result = RunWslc(std::format(L"container exec -e {} {} env", HostEnvVariableName, WslcContainerName));
174 + result.Verify({.Stderr = L"", .ExitCode = 0});
175 +
176 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue)));
177 + }
178 +
179 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile)
180 + {
181 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_FILE_A=exec-env-file-a", "WSLC_TEST_EXEC_ENV_FILE_B=exec-env-file-b"});
182 +
183 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
184 + result.Verify({.Stderr = L"", .ExitCode = 0});
185 +
186 + result = RunWslc(std::format(L"container exec --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
187 + result.Verify({.Stderr = L"", .ExitCode = 0});
188 +
189 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_A=exec-env-file-a"));
190 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_B=exec-env-file-b"));
191 + }
192 +
193 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_MultipleValues)
194 + {
195 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
196 + result.Verify({.Stderr = L"", .ExitCode = 0});
197 +
198 + result = RunWslc(std::format(
199 + L"container exec -e {}=value-a -e {}=value-b {} env", HostEnvVariableName, HostEnvVariableName2, WslcContainerName));
200 + result.Verify({.Stderr = L"", .ExitCode = 0});
201 +
202 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=value-a", HostEnvVariableName)));
203 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=value-b", HostEnvVariableName2)));
204 + }
205 +
206 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_KeyOnly_MultipleValues_UsesHostValues)
207 + {
208 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
209 + result.Verify({.Stderr = L"", .ExitCode = 0});
210 +
211 + result = RunWslc(std::format(L"container exec -e {} -e {} {} env", HostEnvVariableName, HostEnvVariableName2, WslcContainerName));
212 + result.Verify({.Stderr = L"", .ExitCode = 0});
213 +
214 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue)));
215 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName2, HostEnvVariableValue2)));
216 + }
217 +
218 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_EmptyValue)
219 + {
220 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
221 + result.Verify({.Stderr = L"", .ExitCode = 0});
222 +
223 + // Pass an explicit empty value and verify it is present as KEY=
224 + result = RunWslc(std::format(L"container exec -e {}= {} env", HostEnvVariableName, WslcContainerName));
225 + result.Verify({.Stderr = L"", .ExitCode = 0});
226 +
227 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=", HostEnvVariableName)));
228 + }
229 +
230 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_MixedWithEnvFile)
231 + {
232 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_MIX_FILE_A=from-file-a", "WSLC_TEST_EXEC_ENV_MIX_FILE_B=from-file-b"});
233 +
234 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
235 + result.Verify({.Stderr = L"", .ExitCode = 0});
236 +
237 + result = RunWslc(std::format(
238 + L"container exec -e WSLC_TEST_EXEC_ENV_MIX_CLI=from-cli --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
239 + result.Verify({.Stderr = L"", .ExitCode = 0});
240 +
241 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_MIX_FILE_A=from-file-a"));
242 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_MIX_FILE_B=from-file-b"));
243 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_MIX_CLI=from-cli"));
244 + }
245 +
246 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_MissingFile)
247 + {
248 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
249 + result.Verify({.Stderr = L"", .ExitCode = 0});
250 +
251 + result = RunWslc(std::format(L"container exec --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName));
252 + result.Verify(
253 + {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
254 + }
255 +
256 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_MultipleFiles)
257 + {
258 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_FILE_MULTI_A=file1-a", "WSLC_TEST_EXEC_ENV_FILE_MULTI_B=file1-b"});
259 + WriteTestFile(EnvTestFile2, {"WSLC_TEST_EXEC_ENV_FILE_MULTI_C=file2-c", "WSLC_TEST_EXEC_ENV_FILE_MULTI_D=file2-d"});
260 +
261 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
262 + result.Verify({.Stderr = L"", .ExitCode = 0});
263 +
264 + result = RunWslc(std::format(
265 + L"container exec --env-file {} --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), EscapePath(EnvTestFile2.wstring()), WslcContainerName));
266 + result.Verify({.Stderr = L"", .ExitCode = 0});
267 +
268 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_MULTI_A=file1-a"));
269 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_MULTI_B=file1-b"));
270 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_MULTI_C=file2-c"));
271 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_MULTI_D=file2-d"));
272 + }
273 +
274 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_InvalidContent)
275 + {
276 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_VALID=ok", "BAD KEY=value"});
277 +
278 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
279 + result.Verify({.Stderr = L"", .ExitCode = 0});
280 +
281 + result = RunWslc(std::format(L"container exec --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
282 + result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
283 + }
284 +
285 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_DuplicateKeys_Precedence)
286 + {
287 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_DUP=from-file-1"});
288 + WriteTestFile(EnvTestFile2, {"WSLC_TEST_EXEC_ENV_DUP=from-file-2"});
289 +
290 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
291 + result.Verify({.Stderr = L"", .ExitCode = 0});
292 +
293 + // Later --env-file wins
294 + result = RunWslc(std::format(
295 + L"container exec --env-file {} --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), EscapePath(EnvTestFile2.wstring()), WslcContainerName));
296 + result.Verify({.Stderr = L"", .ExitCode = 0});
297 +
298 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_DUP=from-file-2"));
299 +
300 + // Explicit -e wins over env-file
301 + result = RunWslc(std::format(
302 + L"container exec -e WSLC_TEST_EXEC_ENV_DUP=from-cli --env-file {} --env-file {} {} env",
303 + EscapePath(EnvTestFile1.wstring()),
304 + EscapePath(EnvTestFile2.wstring()),
305 + WslcContainerName));
306 + result.Verify({.Stderr = L"", .ExitCode = 0});
307 +
308 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_DUP=from-cli"));
309 + }
310 +
311 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_ValueContainsEquals)
312 + {
313 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_EQUALS=value=with=equals"});
314 +
315 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
316 + result.Verify({.Stderr = L"", .ExitCode = 0});
317 +
318 + result = RunWslc(std::format(L"container exec --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
319 + result.Verify({.Stderr = L"", .ExitCode = 0});
320 +
321 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_EQUALS=value=with=equals"));
322 + }
323 +
324 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_ExitCode_Propagates)
325 + {
326 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
327 + result.Verify({.Stderr = L"", .ExitCode = 0});
328 +
329 + result = RunWslc(std::format(L"container exec {} sh -c \"exit 42\"", WslcContainerName));
330 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 42});
331 + }
332 +
333 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_Stderr_Propagates)
334 + {
335 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
336 + result.Verify({.Stderr = L"", .ExitCode = 0});
337 +
338 + result = RunWslc(std::format(L"container exec {} sh -c \"echo exec-error 1>&2\"", WslcContainerName));
339 + result.Verify({.Stdout = L"", .Stderr = L"exec-error\n", .ExitCode = 0});
340 + }
341 +
342 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_StoppedContainer)
343 + {
344 + auto result = RunWslc(std::format(L"container run --name {} {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
345 + result.Verify({.Stderr = L"", .ExitCode = 0});
346 +
347 + auto inspect = InspectContainer(WslcContainerName);
348 + result = RunWslc(std::format(L"container exec {} echo should-fail", WslcContainerName));
349 + auto errorMessage = std::format(L"Container '{}' is not running.\r\nError code: WSLC_E_CONTAINER_NOT_RUNNING\r\n", inspect.Id);
350 + result.Verify({.Stderr = errorMessage, .ExitCode = 1});
351 + }
352 +
353 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_UserOption_UidRoot)
354 + {
355 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
356 + result.Verify({.Stderr = L"", .ExitCode = 0});
357 +
358 + result = RunWslc(std::format(L"container exec -u 0 {} sh -c \"id -u; id -g\"", WslcContainerName));
359 + result.Verify({.Stdout = L"0\n0\n", .Stderr = L"", .ExitCode = 0});
360 + }
361 +
362 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_UserOption_NameGroupRoot)
363 + {
364 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
365 + result.Verify({.Stderr = L"", .ExitCode = 0});
366 +
367 + result = RunWslc(std::format(L"container exec -u root:root {} sh -c \"id -un; id -u; id -g\"", WslcContainerName));
368 + result.Verify({.Stdout = L"root\n0\n0\n", .Stderr = L"", .ExitCode = 0});
369 + }
370 +
371 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_UserOption_InvalidGroup_Fails)
372 + {
373 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
374 + result.Verify({.Stderr = L"", .ExitCode = 0});
375 +
376 + result = RunWslc(std::format(L"container exec -u root:badgid {} id -u", WslcContainerName));
377 + result.Verify({.Stdout = L"unable to find group badgid: no matching entries in group file\r\n", .ExitCode = 126});
378 + }
379 +
380 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_WorkDir)
381 + {
382 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
383 + result.Verify({.Stderr = L"", .ExitCode = 0});
384 +
385 + result = RunWslc(std::format(L"container exec --workdir /tmp {} pwd", WslcContainerName));
386 + result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
387 + }
388 +
389 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_WorkDir_ShortAlias)
390 + {
391 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
392 + result.Verify({.Stderr = L"", .ExitCode = 0});
393 +
394 + result = RunWslc(std::format(L"container exec -w /tmp {} pwd", WslcContainerName));
395 + result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
396 + }
397 +
398 +private:
399 + const std::wstring WslcContainerName = L"wslc-test-container";
400 + const TestImage& DebianImage = DebianTestImage();
401 +
402 + // Test environment variables
403 + const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
404 + const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
405 + const std::wstring HostEnvVariableValue = L"wslc-host-env-value";
406 + const std::wstring HostEnvVariableValue2 = L"wslc-host-env-value2";
407 + const std::wstring MissingHostEnvVariableName = L"WSLC_TEST_MISSING_HOST_ENV";
408 +
409 + // Test environment variable files
410 + std::filesystem::path EnvTestFile1;
411 + std::filesystem::path EnvTestFile2;
412 +
413 + std::wstring GetHelpMessage() const
414 + {
415 + std::wstringstream output;
416 + output << GetWslcHeader() //
417 + << GetDescription() //
418 + << GetUsage() //
419 + << GetAvailableCommands() //
420 + << GetAvailableOptions();
421 + return output.str();
422 + }
423 +
424 + std::wstring GetDescription() const
425 + {
426 + return L"Executes a command in a running container.\r\n\r\n";
427 + }
428 +
429 + std::wstring GetUsage() const
430 + {
431 + return L"Usage: wslc container exec [<options>] <container-id> <command> [<arguments>...]\r\n\r\n";
432 + }
433 +
434 + std::wstring GetAvailableCommands() const
435 + {
436 + std::wstringstream commands;
437 + commands << L"The following arguments are available:\r\n"
438 + << L" container-id Container ID\r\n"
439 + << L" command The command to run\r\n"
440 + << L" arguments Arguments to pass to the command being executed inside the container\r\n"
441 + << L"\r\n";
442 + return commands.str();
443 + }
444 +
445 + std::wstring GetAvailableOptions() const
446 + {
447 + std::wstringstream options;
448 + options << L"The following options are available:\r\n"
449 + << L" -d,--detach Run container in detached mode\r\n"
450 + << L" -e,--env Key=Value pairs for environment variables\r\n"
451 + << L" --env-file File containing key=value pairs of env variables\r\n"
452 + << L" -i,--interactive Attach to stdin and keep it open\r\n"
453 + << L" --session Specify the session to use\r\n"
454 + << L" -t,--tty Open a TTY with the container process.\r\n"
455 + << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
456 + << L" -w,--workdir Working directory inside the container\r\n"
457 + << L" -?,--help Shows help about the selected command\r\n"
458 + << L"\r\n";
459 + return options.str();
460 + }
461 +};
462 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerInspectTests.cpp new
+160
@@ -0,0 +1,160 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerInspectTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +#include <wslc_schema.h>
19 +
20 +namespace WSLCE2ETests {
21 +using namespace wsl::shared;
22 +using namespace wsl::shared::string;
23 +
24 +class WSLCE2EContainerInspectTests
25 +{
26 + WSLC_TEST_CLASS(WSLCE2EContainerInspectTests)
27 +
28 + TEST_CLASS_SETUP(ClassSetup)
29 + {
30 + EnsureImageIsLoaded(DebianImage);
31 + return true;
32 + }
33 +
34 + TEST_CLASS_CLEANUP(ClassCleanup)
35 + {
36 + EnsureContainerDoesNotExist(TestContainerName1);
37 + EnsureContainerDoesNotExist(TestContainerName2);
38 + EnsureImageIsDeleted(DebianImage);
39 + return true;
40 + }
41 +
42 + TEST_METHOD_SETUP(MethodSetup)
43 + {
44 + EnsureContainerDoesNotExist(TestContainerName1);
45 + EnsureContainerDoesNotExist(TestContainerName2);
46 + return true;
47 + }
48 +
49 + WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_HelpCommand)
50 + {
51 + auto result = RunWslc(L"container inspect --help");
52 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
53 + }
54 +
55 + WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_MissingContainerId)
56 + {
57 + auto result = RunWslc(L"container inspect");
58 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
59 + }
60 +
61 + WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_ContainerNotFound)
62 + {
63 + auto result = RunWslc(std::format(L"container inspect {}", TestContainerName1));
64 + result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Container '{}' not found.\r\n", TestContainerName1), .ExitCode = 1});
65 + }
66 +
67 + WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_Success)
68 + {
69 + auto createResult = RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag()));
70 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
71 +
72 + auto result = RunWslc(std::format(L"container inspect {}", TestContainerName1));
73 + result.Verify({.Stderr = L"", .ExitCode = 0});
74 + auto inspectData =
75 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
76 + VERIFY_ARE_EQUAL(1u, inspectData.size());
77 + VERIFY_ARE_EQUAL(WideToMultiByte(TestContainerName1), inspectData[0].Name);
78 + }
79 +
80 + WSLC_TEST_METHOD(WSLCE2E_Container_InspectMultiple_Success)
81 + {
82 + // Create two containers to inspect at the same time
83 + auto result = RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag()));
84 + result.Verify({.Stderr = L"", .ExitCode = 0});
85 + result = RunWslc(std::format(L"container create --name {} {}", TestContainerName2, DebianImage.NameAndTag()));
86 + result.Verify({.Stderr = L"", .ExitCode = 0});
87 +
88 + // Inspect both containers in the same command
89 + result = RunWslc(std::format(L"container inspect {} {}", TestContainerName1, TestContainerName2));
90 + result.Verify({.Stderr = L"", .ExitCode = 0});
91 + auto inspectData =
92 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
93 + VERIFY_ARE_EQUAL(2u, inspectData.size());
94 + VERIFY_ARE_EQUAL(WideToMultiByte(TestContainerName1), inspectData[0].Name);
95 + VERIFY_ARE_EQUAL(WideToMultiByte(TestContainerName2), inspectData[1].Name);
96 + }
97 +
98 + WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_MixedFoundNotFound)
99 + {
100 + // Create one container but not the other
101 + auto result = RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag()));
102 + result.Verify({.Stderr = L"", .ExitCode = 0});
103 +
104 + // Inspect both containers in the same command, expecting one to be found and the other to not be found
105 + result = RunWslc(std::format(L"container inspect {} {}", TestContainerName1, TestContainerName2));
106 + result.Verify({.Stderr = std::format(L"Container '{}' not found.\r\n", TestContainerName2), .ExitCode = 1});
107 +
108 + // Verify found container
109 + auto inspectData =
110 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
111 + VERIFY_ARE_EQUAL(1u, inspectData.size());
112 + VERIFY_ARE_EQUAL(WideToMultiByte(TestContainerName1), inspectData[0].Name);
113 + }
114 +
115 +private:
116 + const std::wstring TestContainerName1 = L"wslc-e2e-container-inspect-1";
117 + const std::wstring TestContainerName2 = L"wslc-e2e-container-inspect-2";
118 + const TestImage& DebianImage = DebianTestImage();
119 +
120 + std::wstring GetHelpMessage() const
121 + {
122 + std::wstringstream output;
123 + output << GetWslcHeader() //
124 + << GetDescription() //
125 + << GetUsage() //
126 + << GetAvailableCommands() //
127 + << GetAvailableOptions();
128 + return output.str();
129 + }
130 +
131 + std::wstring GetDescription() const
132 + {
133 + return Localization::WSLCCLI_ContainerInspectLongDesc() + L"\r\n\r\n";
134 + }
135 +
136 + std::wstring GetUsage() const
137 + {
138 + return L"Usage: wslc container inspect [<options>] <container-id>\r\n\r\n";
139 + }
140 +
141 + std::wstring GetAvailableCommands() const
142 + {
143 + std::wstringstream commands;
144 + commands << L"The following arguments are available:\r\n" //
145 + << L" container-id Container ID\r\n" //
146 + << L"\r\n";
147 + return commands.str();
148 + }
149 +
150 + std::wstring GetAvailableOptions() const
151 + {
152 + std::wstringstream options;
153 + options << L"The following options are available:\r\n" //
154 + << L" --session Specify the session to use\r\n" //
155 + << L" -?,--help Shows help about the selected command\r\n" //
156 + << L"\r\n";
157 + return options.str();
158 + }
159 +};
160 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerKillTests.cpp new
+188
@@ -0,0 +1,188 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerKillTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21 +
22 +class WSLCE2EContainerKillTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EContainerKillTests)
25 +
26 + TEST_CLASS_SETUP(ClassSetup)
27 + {
28 + EnsureImageIsLoaded(DebianImage);
29 + return true;
30 + }
31 +
32 + TEST_CLASS_CLEANUP(ClassCleanup)
33 + {
34 + EnsureContainerDoesNotExist(WslcContainerName);
35 + EnsureContainerDoesNotExist(WslcContainerName2);
36 + EnsureImageIsDeleted(DebianImage);
37 + return true;
38 + }
39 +
40 + TEST_METHOD_SETUP(TestMethodSetup)
41 + {
42 + EnsureContainerDoesNotExist(WslcContainerName);
43 + EnsureContainerDoesNotExist(WslcContainerName2);
44 + return true;
45 + }
46 +
47 + WSLC_TEST_METHOD(WSLCE2E_Container_Kill_HelpCommand)
48 + {
49 + auto result = RunWslc(L"container kill --help");
50 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
51 + }
52 +
53 + WSLC_TEST_METHOD(WSLCE2E_Container_Kill_KillsRunningContainer)
54 + {
55 + // Run a container in the background
56 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
57 + result.Verify({.Stderr = L"", .ExitCode = 0});
58 + auto containerId = result.GetStdoutOneLine();
59 + VERIFY_IS_FALSE(containerId.empty());
60 +
61 + // Verify container is running
62 + VerifyContainerIsListed(containerId, L"running");
63 +
64 + // Kill the container
65 + result = RunWslc(std::format(L"container kill {}", containerId));
66 + result.Verify({.Stderr = L"", .ExitCode = 0});
67 +
68 + // Verify the container is no longer running
69 + VerifyContainerIsListed(containerId, L"exited");
70 + }
71 +
72 + WSLC_TEST_METHOD(WSLCE2E_Container_Kill_ByName)
73 + {
74 + // Run a container in the background
75 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
76 + result.Verify({.Stderr = L"", .ExitCode = 0});
77 + const auto containerId = result.GetStdoutOneLine();
78 + VERIFY_IS_FALSE(containerId.empty());
79 +
80 + // Verify container is running
81 + VerifyContainerIsListed(containerId, L"running");
82 +
83 + // Kill by container name
84 + result = RunWslc(std::format(L"container kill {}", WslcContainerName));
85 + result.Verify({.Stderr = L"", .ExitCode = 0});
86 +
87 + // Verify container is no longer running
88 + VerifyContainerIsListed(containerId, L"exited");
89 + }
90 +
91 + WSLC_TEST_METHOD(WSLCE2E_Container_Kill_NotFound)
92 + {
93 + VerifyContainerIsNotListed(WslcContainerName);
94 +
95 + auto result = RunWslc(std::format(L"container kill {}", WslcContainerName));
96 + result.Verify(
97 + {.Stderr = std::format(L"Container '{}' not found.\r\nError code: WSLC_E_CONTAINER_NOT_FOUND\r\n", WslcContainerName),
98 + .ExitCode = 1});
99 + }
100 +
101 + WSLC_TEST_METHOD(WSLCE2E_Container_Kill_InvalidSignal)
102 + {
103 + auto result = RunWslc(std::format(L"container run --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
104 + result.Verify({.Stderr = L"", .ExitCode = 0});
105 +
106 + {
107 + result = RunWslc(std::format(L"container kill {} -s 0", WslcContainerName));
108 + result.Verify({.Stderr = L"Invalid signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
109 + }
110 +
111 + {
112 + result = RunWslc(std::format(L"container kill {} -s 32", WslcContainerName));
113 + result.Verify({.Stderr = L"Invalid signal value: 32 is out of valid range (1-31).\r\n", .ExitCode = 1});
114 + }
115 + }
116 +
117 + WSLC_TEST_METHOD(WSLCE2E_Container_Kill_TargetedContainerOnly)
118 + {
119 + // Run first container in background
120 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
121 + result.Verify({.Stderr = L"", .ExitCode = 0});
122 + const auto firstContainerId = result.GetStdoutOneLine();
123 + VERIFY_IS_FALSE(firstContainerId.empty());
124 +
125 + // Run second container in background
126 + result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName2, DebianImage.NameAndTag()));
127 + result.Verify({.Stderr = L"", .ExitCode = 0});
128 + const auto secondContainerId = result.GetStdoutOneLine();
129 + VERIFY_IS_FALSE(secondContainerId.empty());
130 +
131 + // Verify both are running
132 + VerifyContainerIsListed(firstContainerId, L"running");
133 + VerifyContainerIsListed(secondContainerId, L"running");
134 +
135 + // Kill only the first container
136 + result = RunWslc(std::format(L"container kill {}", firstContainerId));
137 + result.Verify({.Stderr = L"", .ExitCode = 0});
138 +
139 + // Verify first exited, second still running
140 + VerifyContainerIsListed(firstContainerId, L"exited");
141 + VerifyContainerIsListed(secondContainerId, L"running");
142 + }
143 +
144 +private:
145 + const std::wstring WslcContainerName = L"wslc-test-container";
146 + const std::wstring WslcContainerName2 = L"wslc-test-container-2";
147 + const TestImage& DebianImage = DebianTestImage();
148 +
149 + std::wstring GetHelpMessage() const
150 + {
151 + std::wstringstream output;
152 + output << GetWslcHeader() //
153 + << GetDescription() //
154 + << GetUsage() //
155 + << GetAvailableCommands() //
156 + << GetAvailableOptions();
157 + return output.str();
158 + }
159 +
160 + std::wstring GetDescription() const
161 + {
162 + return Localization::WSLCCLI_ContainerKillLongDesc() + L"\r\n\r\n";
163 + }
164 +
165 + std::wstring GetUsage() const
166 + {
167 + return L"Usage: wslc container kill [<options>] <container-id>\r\n\r\n";
168 + }
169 +
170 + std::wstring GetAvailableCommands() const
171 + {
172 + std::wstringstream commands;
173 + commands << L"The following arguments are available:\r\n" << L" container-id Container ID\r\n" << L"\r\n";
174 + return commands.str();
175 + }
176 +
177 + std::wstring GetAvailableOptions() const
178 + {
179 + std::wstringstream options;
180 + options << L"The following options are available:\r\n"
181 + << L" --session Specify the session to use\r\n"
182 + << L" -s,--signal Signal to send (default: SIGKILL)\r\n"
183 + << L" -?,--help Shows help about the selected command\r\n"
184 + << L"\r\n";
185 + return options.str();
186 + }
187 +};
188 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp new
+251
@@ -0,0 +1,251 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerListTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "ContainerModel.h"
17 +#include "WSLCExecutor.h"
18 +#include "WSLCE2EHelpers.h"
19 +
20 +namespace WSLCE2ETests {
21 +using namespace wsl::shared;
22 +
23 +using namespace wsl::windows::wslc::models;
24 +using namespace wsl::windows::common::string;
25 +
26 +class WSLCE2EContainerListTests
27 +{
28 + WSLC_TEST_CLASS(WSLCE2EContainerListTests)
29 +
30 + TEST_CLASS_SETUP(ClassSetup)
31 + {
32 + EnsureImageIsLoaded(DebianImage);
33 + return true;
34 + }
35 +
36 + TEST_CLASS_CLEANUP(ClassCleanup)
37 + {
38 + EnsureContainerDoesNotExist(WslcContainerName);
39 + EnsureContainerDoesNotExist(WslcContainerName2);
40 + EnsureImageIsDeleted(DebianImage);
41 + return true;
42 + }
43 +
44 + TEST_METHOD_SETUP(TestMethodSetup)
45 + {
46 + EnsureContainerDoesNotExist(WslcContainerName);
47 + EnsureContainerDoesNotExist(WslcContainerName2);
48 + return true;
49 + }
50 +
51 + WSLC_TEST_METHOD(WSLCE2E_Container_List_HelpCommand)
52 + {
53 + auto result = RunWslc(L"container list --help");
54 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
55 + }
56 +
57 + WSLC_TEST_METHOD(WSLCE2E_Container_List_AllOption)
58 + {
59 + VerifyContainerIsNotListed(WslcContainerName);
60 +
61 + // Create a container
62 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
63 + result.Verify({.Stderr = L"", .ExitCode = 0});
64 + auto containerId = result.GetStdoutOneLine();
65 + VERIFY_IS_FALSE(containerId.empty());
66 +
67 + // Find container in list output
68 + result = RunWslc(L"container list --no-trunc --all");
69 + result.Verify({.Stderr = L"", .ExitCode = 0});
70 + auto outputLines = result.GetStdoutLines();
71 + std::optional<std::wstring> foundContainerLine{};
72 + for (const auto& line : outputLines)
73 + {
74 + if (line.find(containerId) != std::wstring::npos)
75 + {
76 + foundContainerLine = line;
77 + break;
78 + }
79 + }
80 +
81 + // Verify we found the container in the list output
82 + VERIFY_IS_TRUE(foundContainerLine.has_value());
83 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, foundContainerLine->find(L"created"));
84 + }
85 +
86 + WSLC_TEST_METHOD(WSLCE2E_Container_List_NoOptions_RunningContainers)
87 + {
88 + VerifyContainerIsNotListed(WslcContainerName);
89 +
90 + // Run a container in the background
91 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
92 + result.Verify({.Stderr = L"", .ExitCode = 0});
93 + auto containerId = TruncateId(result.GetStdoutOneLine());
94 + VERIFY_IS_FALSE(containerId.empty());
95 +
96 + // Find container in list output with no options
97 + result = RunWslc(L"container list");
98 + result.Verify({.Stderr = L"", .ExitCode = 0});
99 + auto outputLines = result.GetStdoutLines();
100 + std::optional<std::wstring> foundContainerLine{};
101 + for (const auto& line : outputLines)
102 + {
103 + if (line.find(containerId) != std::wstring::npos)
104 + {
105 + foundContainerLine = line;
106 + break;
107 + }
108 + }
109 +
110 + // Verify we found the container in the list output
111 + VERIFY_IS_TRUE(foundContainerLine.has_value());
112 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, foundContainerLine->find(L"running"));
113 + }
114 +
115 + WSLC_TEST_METHOD(WSLCE2E_Container_List_NoOptions_ExcludesCreatedContainers)
116 + {
117 + VerifyContainerIsNotListed(WslcContainerName);
118 +
119 + // Create (but do not start) a container.
120 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
121 + result.Verify({.Stderr = L"", .ExitCode = 0});
122 + const auto containerId = TruncateId(result.GetStdoutOneLine());
123 + VERIFY_IS_FALSE(containerId.empty());
124 +
125 + // Default list should only show running containers.
126 + result = RunWslc(L"container list");
127 + result.Verify({.Stderr = L"", .ExitCode = 0});
128 + bool isListed = false;
129 + for (const auto& line : result.GetStdoutLines())
130 + {
131 + if (line.find(containerId) != std::wstring::npos)
132 + {
133 + isListed = true;
134 + break;
135 + }
136 + }
137 +
138 + VERIFY_IS_FALSE(isListed);
139 + }
140 +
141 + WSLC_TEST_METHOD(WSLCE2E_Container_List_QuietOption_OutputsIdsOnly)
142 + {
143 + VerifyContainerIsNotListed(WslcContainerName);
144 +
145 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
146 + result.Verify({.Stderr = L"", .ExitCode = 0});
147 + const auto containerId = result.GetStdoutOneLine();
148 + VERIFY_IS_FALSE(containerId.empty());
149 +
150 + result = RunWslc(L"container list --all --quiet");
151 + result.Verify({.Stderr = L"", .ExitCode = 0});
152 + const auto outputLine = result.GetStdoutOneLine();
153 +
154 + VERIFY_ARE_EQUAL(containerId, outputLine);
155 + }
156 +
157 + WSLC_TEST_METHOD(WSLCE2E_Container_List_InvalidFormatOption)
158 + {
159 + const auto result = RunWslc(L"container list --format invalid");
160 + result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1});
161 + }
162 +
163 + WSLC_TEST_METHOD(WSLCE2E_Container_List_JsonFormat)
164 + {
165 + VerifyContainerIsNotListed(WslcContainerName);
166 +
167 + // Create a container
168 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
169 + result.Verify({.Stderr = L"", .ExitCode = 0});
170 + const auto containerId = result.GetStdoutOneLine();
171 + VERIFY_IS_FALSE(containerId.empty());
172 +
173 + // List containers with json format
174 + result = RunWslc(L"container list --all --format json");
175 + result.Verify({.Stderr = L"", .ExitCode = 0});
176 + // Parse json and verify we got the expected container information back
177 + auto containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(result.Stdout.value().c_str());
178 + VERIFY_ARE_EQUAL(1U, containers.size());
179 + VERIFY_ARE_EQUAL(containerId, wsl::shared::string::MultiByteToWide(containers[0].Id));
180 +
181 + // Create another container
182 + result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag()));
183 + result.Verify({.Stderr = L"", .ExitCode = 0});
184 + const auto containerId2 = result.GetStdoutOneLine();
185 + VERIFY_IS_FALSE(containerId2.empty());
186 +
187 + // List containers with json format again
188 + result = RunWslc(L"container list --all --format json");
189 + result.Verify({.Stderr = L"", .ExitCode = 0});
190 + // Parse json and verify we got both containers back
191 + containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(result.Stdout.value().c_str());
192 + VERIFY_ARE_EQUAL(2U, containers.size());
193 +
194 + // Extract container IDs
195 + std::vector<std::wstring> containerIds;
196 + for (const auto& container : containers)
197 + {
198 + containerIds.push_back(wsl::shared::string::MultiByteToWide(container.Id));
199 + }
200 +
201 + // Verify both container IDs are in the list
202 + VERIFY_IS_TRUE(std::find(containerIds.begin(), containerIds.end(), containerId) != containerIds.end());
203 + VERIFY_IS_TRUE(std::find(containerIds.begin(), containerIds.end(), containerId2) != containerIds.end());
204 + }
205 +
206 +private:
207 + const std::wstring WslcContainerName = L"wslc-test-container";
208 + const std::wstring WslcContainerName2 = L"wslc-test-container-2";
209 + const TestImage& DebianImage = DebianTestImage();
210 +
211 + std::wstring GetHelpMessage() const
212 + {
213 + std::wstringstream output;
214 + output << GetWslcHeader() //
215 + << GetDescription() //
216 + << GetUsage() //
217 + << GetAvailableCommandAliases() //
218 + << GetAvailableOptions();
219 + return output.str();
220 + }
221 +
222 + std::wstring GetDescription() const
223 + {
224 + return Localization::WSLCCLI_ContainerListLongDesc() + L"\r\n\r\n";
225 + }
226 +
227 + std::wstring GetUsage() const
228 + {
229 + return L"Usage: wslc container list [<options>]\r\n\r\n";
230 + }
231 +
232 + std::wstring GetAvailableCommandAliases() const
233 + {
234 + return L"The following command aliases are available: ls ps\r\n\r\n";
235 + }
236 +
237 + std::wstring GetAvailableOptions() const
238 + {
239 + std::wstringstream options;
240 + options << L"The following options are available:\r\n"
241 + << L" -a,--all Show all regardless of state.\r\n"
242 + << L" --format " << Localization::WSLCCLI_FormatArgDescription() << L"\r\n"
243 + << L" --no-trunc Do not truncate output\r\n"
244 + << L" -q,--quiet Outputs the container IDs only\r\n"
245 + << L" --session Specify the session to use\r\n"
246 + << L" -?,--help Shows help about the selected command\r\n"
247 + << L"\r\n";
248 + return options.str();
249 + }
250 +};
251 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EContainerRemoveTests.cpp new
+209
@@ -0,0 +1,209 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerRemoveTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21 +
22 +class WSLCE2EContainerRemoveTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EContainerRemoveTests)
25 +
26 + TEST_CLASS_SETUP(ClassSetup)
27 + {
28 + EnsureImageIsLoaded(DebianImage);
29 + return true;
30 + }
31 +
32 + TEST_CLASS_CLEANUP(ClassCleanup)
33 + {
34 + EnsureContainerDoesNotExist(WslcContainerName);
35 + EnsureContainerDoesNotExist(WslcContainerName2);
36 + EnsureImageIsDeleted(DebianImage);
37 + return true;
38 + }
39 +
40 + TEST_METHOD_SETUP(TestMethodSetup)
41 + {
42 + EnsureContainerDoesNotExist(WslcContainerName);
43 + EnsureContainerDoesNotExist(WslcContainerName2);
44 + return true;
45 + }
46 +
47 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_HelpCommand)
48 + {
49 + auto result = RunWslc(L"container remove --help");
50 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
51 + }
52 +
53 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_NotFound)
54 + {
55 + VerifyContainerIsNotListed(WslcContainerName);
56 +
57 + auto result = RunWslc(std::format(L"container remove {}", WslcContainerName));
58 + result.Verify(
59 + {.Stdout = L"",
60 + .Stderr = std::format(L"Container '{}' not found.\r\nError code: WSLC_E_CONTAINER_NOT_FOUND\r\n", WslcContainerName),
61 + .ExitCode = 1});
62 + }
63 +
64 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_Valid)
65 + {
66 + VerifyContainerIsNotListed(WslcContainerName);
67 +
68 + // Create the container with a valid image
69 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
70 + result.Verify({.Stderr = L"", .ExitCode = 0});
71 + std::wstring containerId = result.GetStdoutOneLine();
72 +
73 + // Verify the container is listed with the correct status
74 + VerifyContainerIsListed(containerId, L"created");
75 +
76 + // Delete the container
77 + result = RunWslc(std::format(L"container remove {}", WslcContainerName));
78 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
79 +
80 + // Verify the container is no longer listed
81 + VerifyContainerIsNotListed(WslcContainerName);
82 + }
83 +
84 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_ById_Valid)
85 + {
86 + VerifyContainerIsNotListed(WslcContainerName);
87 +
88 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
89 + result.Verify({.Stderr = L"", .ExitCode = 0});
90 + const auto containerId = result.GetStdoutOneLine();
91 + VERIFY_IS_FALSE(containerId.empty());
92 +
93 + VerifyContainerIsListed(containerId, L"created");
94 +
95 + result = RunWslc(std::format(L"container remove {}", containerId));
96 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
97 +
98 + VerifyContainerIsNotListed(containerId);
99 + VerifyContainerIsNotListed(WslcContainerName);
100 + }
101 +
102 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_Force_RunningContainer)
103 + {
104 + VerifyContainerIsNotListed(WslcContainerName);
105 +
106 + // Run a container so it is in running state
107 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
108 + result.Verify({.Stderr = L"", .ExitCode = 0});
109 + const auto containerId = result.GetStdoutOneLine();
110 + VERIFY_IS_FALSE(containerId.empty());
111 +
112 + VerifyContainerIsListed(containerId, L"running");
113 +
114 + // Removing without force should fail
115 + result = RunWslc(std::format(L"container remove {}", containerId));
116 +
117 + // TODO Add .Stderr after this issue is resolved:
118 + // https://github.com/microsoft/WSL/issues/14510
119 + result.Verify({.ExitCode = 1});
120 +
121 + // Container should still exist and be running
122 + VerifyContainerIsListed(containerId, L"running");
123 +
124 + // Removing with force should succeed
125 + result = RunWslc(std::format(L"container remove --force {}", containerId));
126 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
127 +
128 + VerifyContainerIsNotListed(containerId);
129 + VerifyContainerIsNotListed(WslcContainerName);
130 + }
131 +
132 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_Multiple_Valid)
133 + {
134 + VerifyContainerIsNotListed(WslcContainerName);
135 + VerifyContainerIsNotListed(WslcContainerName2);
136 +
137 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
138 + result.Verify({.Stderr = L"", .ExitCode = 0});
139 + const auto containerId1 = result.GetStdoutOneLine();
140 + VERIFY_IS_FALSE(containerId1.empty());
141 +
142 + result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag()));
143 + result.Verify({.Stderr = L"", .ExitCode = 0});
144 + const auto containerId2 = result.GetStdoutOneLine();
145 + VERIFY_IS_FALSE(containerId2.empty());
146 +
147 + VerifyContainerIsListed(containerId1, L"created");
148 + VerifyContainerIsListed(containerId2, L"created");
149 +
150 + result = RunWslc(std::format(L"container remove {} {}", containerId1, containerId2));
151 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
152 +
153 + VerifyContainerIsNotListed(containerId1);
154 + VerifyContainerIsNotListed(containerId2);
155 + VerifyContainerIsNotListed(WslcContainerName);
156 + VerifyContainerIsNotListed(WslcContainerName2);
157 + }
158 +
159 +private:
160 + const std::wstring WslcContainerName = L"wslc-test-container";
161 + const std::wstring WslcContainerName2 = L"wslc-test-container-2";
162 + const TestImage& DebianImage = DebianTestImage();
163 +
164 + std::wstring GetHelpMessage() const
165 + {
166 + std::wstringstream output;
167 + output << GetWslcHeader() //
168 + << GetDescription() //
169 + << GetUsage() //
170 + << GetAvailableCommandAliases() //
171 + << GetAvailableCommands() //
172 + << GetAvailableOptions();
173 + return output.str();
174 + }
175 +
176 + std::wstring GetDescription() const
177 + {
178 + return Localization::WSLCCLI_ContainerRemoveLongDesc() + L"\r\n\r\n";
179 + }
180 +
181 + std::wstring GetUsage() const
182 + {
183 + return L"Usage: wslc container remove [<options>] <container-id>\r\n\r\n";
184 + }
185 +
186 + std::wstring GetAvailableCommandAliases() const
187 + {
188 + return L"The following command aliases are available: delete rm\r\n\r\n";
189 + }
190 +
191 + std::wstring GetAvailableCommands() const
192 + {
193 + std::wstringstream commands;
194 + commands << L"The following arguments are available:\r\n" << L" container-id Container ID\r\n" << L"\r\n";
195 + return commands.str();
196 + }
197 +
198 + std::wstring GetAvailableOptions() const
199 + {
200 + std::wstringstream options;
201 + options << L"The following options are available:\r\n" //
202 + << L" -f,--force Delete containers even if they are running\r\n"
203 + << L" --session Specify the session to use\r\n"
204 + << L" -?,--help Shows help about the selected command\r\n"
205 + << L"\r\n";
206 + return options.str();
207 + }
208 +};
209 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp new
+745
@@ -0,0 +1,745 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerRunTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +
21 +class WSLCE2EContainerRunTests
22 +{
23 + WSLC_TEST_CLASS(WSLCE2EContainerRunTests)
24 +
25 + TEST_CLASS_SETUP(ClassSetup)
26 + {
27 + EnsureImageIsLoaded(DebianImage);
28 + EnsureImageIsLoaded(PythonImage);
29 +
30 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), HostEnvVariableValue.c_str()));
31 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), HostEnvVariableValue2.c_str()));
32 +
33 + // Initialize Winsock for loopback connectivity tests
34 + WSADATA wsaData{};
35 + const int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
36 + THROW_HR_IF(HRESULT_FROM_WIN32(result), result != 0);
37 + return true;
38 + }
39 +
40 + TEST_CLASS_CLEANUP(ClassCleanup)
41 + {
42 + EnsureContainerDoesNotExist(WslcContainerName);
43 + EnsureContainerDoesNotExist(WslcContainerName2);
44 + EnsureImageIsDeleted(DebianImage);
45 + EnsureImageIsDeleted(PythonImage);
46 + EnsureVolumeDoesNotExist(WslcVolumeName);
47 +
48 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
49 + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr));
50 +
51 + // Cleanup Winsock
52 + WSACleanup();
53 + return true;
54 + }
55 +
56 + TEST_METHOD_SETUP(TestMethodSetup)
57 + {
58 + EnsureContainerDoesNotExist(WslcContainerName);
59 + EnsureContainerDoesNotExist(WslcContainerName2);
60 + EnsureVolumeDoesNotExist(WslcVolumeName);
61 +
62 + EnvTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
63 + EnvTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
64 + return true;
65 + }
66 +
67 + TEST_METHOD_CLEANUP(TestMethodCleanup)
68 + {
69 + DeleteFileW(EnvTestFile1.c_str());
70 + DeleteFileW(EnvTestFile2.c_str());
71 + return true;
72 + }
73 +
74 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_HelpCommand)
75 + {
76 + auto result = RunWslc(L"container run --help");
77 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
78 + }
79 +
80 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Container_With_Command)
81 + {
82 + VerifyContainerIsNotListed(WslcContainerName);
83 +
84 + auto command = L"echo echo_from_container";
85 + auto result = RunWslc(std::format(L"container run --name {} {} {}", WslcContainerName, DebianImage.NameAndTag(), command));
86 + result.Verify({.Stdout = L"echo_from_container\n", .Stderr = L"", .ExitCode = 0});
87 +
88 + VerifyContainerIsListed(WslcContainerName, L"exited");
89 + }
90 +
91 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Entrypoint)
92 + {
93 + auto result = RunWslc(std::format(L"container run --rm --entrypoint /bin/whoami {}", DebianImage.NameAndTag()));
94 + result.Verify({.Stdout = L"root\n", .Stderr = L"", .ExitCode = 0});
95 + }
96 +
97 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Entrypoint_And_Arguments)
98 + {
99 + auto result = RunWslc(
100 + std::format(L"container run --rm --entrypoint /bin/echo {} hello from entrypoint with args", DebianImage.NameAndTag()));
101 + result.Verify({.Stdout = L"hello from entrypoint with args\n", .Stderr = L"", .ExitCode = 0});
102 + }
103 +
104 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Entrypoint_Invalid_Path)
105 + {
106 + auto result = RunWslc(std::format(L"container run --rm --entrypoint /bin/does-not-exist {}", DebianImage.NameAndTag()));
107 + result.Verify(
108 + {.Stdout = L"", .Stderr = L"failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: \"/bin/does-not-exist\": stat /bin/does-not-exist: no such file or directory: unknown\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
109 + }
110 +
111 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Entrypoint_Detach_Lifecycle)
112 + {
113 + auto result = RunWslc(std::format(
114 + L"container run --name {} -d --entrypoint /bin/sleep {} infinity", WslcContainerName, DebianImage.NameAndTag()));
115 + result.Verify({.Stderr = L"", .ExitCode = 0});
116 +
117 + VerifyContainerIsListed(WslcContainerName, L"running");
118 + }
119 +
120 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Remove)
121 + {
122 + VerifyContainerIsNotListed(WslcContainerName);
123 +
124 + // Run the container with a valid image
125 + auto result = RunWslc(std::format(L"container run --rm --name {} {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
126 + result.Verify({.Stderr = L"", .ExitCode = 0});
127 +
128 + // Run should be deleted on return so no retry.
129 + VerifyContainerIsNotListed(WslcContainerName);
130 + }
131 +
132 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption)
133 + {
134 + VerifyContainerIsNotListed(WslcContainerName);
135 +
136 + auto result = RunWslc(std::format(
137 + L"container run --rm --name {} -e {}=A {} env", WslcContainerName, HostEnvVariableName, DebianImage.NameAndTag()));
138 + result.Verify({.Stderr = L"", .ExitCode = 0});
139 +
140 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=A", HostEnvVariableName)));
141 + }
142 +
143 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_MultipleValues)
144 + {
145 + VerifyContainerIsNotListed(WslcContainerName);
146 +
147 + auto result = RunWslc(std::format(
148 + L"container run --rm --name {} -e {}=A -e {}=B {} env",
149 + WslcContainerName,
150 + HostEnvVariableName,
151 + HostEnvVariableName2,
152 + DebianImage.NameAndTag()));
153 + result.Verify({.Stderr = L"", .ExitCode = 0});
154 +
155 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=A", HostEnvVariableName)));
156 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=B", HostEnvVariableName2)));
157 + }
158 +
159 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_KeyOnly_UsesHostValue)
160 + {
161 + VerifyContainerIsNotListed(WslcContainerName);
162 +
163 + auto result = RunWslc(std::format(
164 + L"container run --rm --name {} -e {} {} env", WslcContainerName, HostEnvVariableName, DebianImage.NameAndTag()));
165 + result.Verify({.Stderr = L"", .ExitCode = 0});
166 +
167 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue)));
168 + }
169 +
170 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_KeyOnly_MultipleValues_UsesHostValues)
171 + {
172 + VerifyContainerIsNotListed(WslcContainerName);
173 +
174 + auto result = RunWslc(std::format(
175 + L"container run --rm --name {} -e {} -e {} {} env",
176 + WslcContainerName,
177 + HostEnvVariableName,
178 + HostEnvVariableName2,
179 + DebianImage.NameAndTag()));
180 + result.Verify({.Stderr = L"", .ExitCode = 0});
181 +
182 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue)));
183 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName2, HostEnvVariableValue2)));
184 + }
185 +
186 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_EmptyValue)
187 + {
188 + VerifyContainerIsNotListed(WslcContainerName);
189 +
190 + auto result = RunWslc(std::format(
191 + L"container run --rm --name {} -e {}= {} env", WslcContainerName, HostEnvVariableName, DebianImage.NameAndTag()));
192 + result.Verify({.Stderr = L"", .ExitCode = 0});
193 +
194 + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=", HostEnvVariableName)));
195 + }
196 +
197 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile)
198 + {
199 + VerifyContainerIsNotListed(WslcContainerName);
200 +
201 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_FILE_A=env-file-a", "WSLC_TEST_ENV_FILE_B=env-file-b"});
202 +
203 + auto result = RunWslc(std::format(
204 + L"container run --rm --name {} --env-file {} {} env",
205 + WslcContainerName,
206 + EscapePath(EnvTestFile1.wstring()),
207 + DebianImage.NameAndTag()));
208 + result.Verify({.Stderr = L"", .ExitCode = 0});
209 +
210 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_A=env-file-a"));
211 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_B=env-file-b"));
212 + }
213 +
214 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_MixedWithEnvFile)
215 + {
216 + VerifyContainerIsNotListed(WslcContainerName);
217 +
218 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_MIX_FILE_A=from-file-a", "WSLC_TEST_ENV_MIX_FILE_B=from-file-b"});
219 +
220 + auto result = RunWslc(std::format(
221 + L"container run --rm --name {} -e WSLC_TEST_ENV_MIX_CLI=from-cli --env-file {} {} env",
222 + WslcContainerName,
223 + EscapePath(EnvTestFile1.wstring()),
224 + DebianImage.NameAndTag()));
225 + result.Verify({.Stderr = L"", .ExitCode = 0});
226 +
227 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_MIX_FILE_A=from-file-a"));
228 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_MIX_FILE_B=from-file-b"));
229 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_MIX_CLI=from-cli"));
230 + }
231 +
232 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_MultipleFiles)
233 + {
234 + VerifyContainerIsNotListed(WslcContainerName);
235 +
236 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_FILE_MULTI_A=file1-a", "WSLC_TEST_ENV_FILE_MULTI_B=file1-b"});
237 +
238 + WriteTestFile(EnvTestFile2, {"WSLC_TEST_ENV_FILE_MULTI_C=file2-c", "WSLC_TEST_ENV_FILE_MULTI_D=file2-d"});
239 +
240 + auto result = RunWslc(std::format(
241 + L"container run --rm --name {} --env-file {} --env-file {} {} env",
242 + WslcContainerName,
243 + EscapePath(EnvTestFile1.wstring()),
244 + EscapePath(EnvTestFile2.wstring()),
245 + DebianImage.NameAndTag()));
246 + result.Verify({.Stderr = L"", .ExitCode = 0});
247 +
248 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_MULTI_A=file1-a"));
249 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_MULTI_B=file1-b"));
250 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_MULTI_C=file2-c"));
251 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_MULTI_D=file2-d"));
252 + }
253 +
254 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_MissingFile)
255 + {
256 + VerifyContainerIsNotListed(WslcContainerName);
257 +
258 + auto result = RunWslc(std::format(
259 + L"container run --rm --name {} --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName, DebianImage.NameAndTag()));
260 + result.Verify(
261 + {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
262 + }
263 +
264 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_InvalidContent)
265 + {
266 + VerifyContainerIsNotListed(WslcContainerName);
267 +
268 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_VALID=ok", "BAD KEY=value"});
269 +
270 + auto result = RunWslc(std::format(
271 + L"container run --rm --name {} --env-file {} {} env",
272 + WslcContainerName,
273 + EscapePath(EnvTestFile1.wstring()),
274 + DebianImage.NameAndTag()));
275 + result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
276 + }
277 +
278 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_DuplicateKeys_Precedence)
279 + {
280 + VerifyContainerIsNotListed(WslcContainerName);
281 +
282 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_DUP=from-file-1"});
283 +
284 + WriteTestFile(EnvTestFile2, {"WSLC_TEST_ENV_DUP=from-file-2"});
285 +
286 + // Later --env-file should win over earlier --env-file for duplicate keys
287 + auto result = RunWslc(std::format(
288 + L"container run --rm --name {} --env-file {} --env-file {} {} env",
289 + WslcContainerName,
290 + EscapePath(EnvTestFile1.wstring()),
291 + EscapePath(EnvTestFile2.wstring()),
292 + DebianImage.NameAndTag()));
293 + result.Verify({.Stderr = L"", .ExitCode = 0});
294 +
295 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_DUP=from-file-2"));
296 +
297 + // Explicit -e should win over env-file value for duplicate keys
298 + result = RunWslc(std::format(
299 + L"container run --rm --name {} -e WSLC_TEST_ENV_DUP=from-cli --env-file {} --env-file {} {} env",
300 + WslcContainerName,
301 + EscapePath(EnvTestFile1.wstring()),
302 + EscapePath(EnvTestFile2.wstring()),
303 + DebianImage.NameAndTag()));
304 + result.Verify({.Stderr = L"", .ExitCode = 0});
305 +
306 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_DUP=from-cli"));
307 + }
308 +
309 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_ValueContainsEquals)
310 + {
311 + VerifyContainerIsNotListed(WslcContainerName);
312 +
313 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_EQUALS=value=with=equals"});
314 +
315 + auto result = RunWslc(std::format(
316 + L"container run --rm --name {} --env-file {} {} env",
317 + WslcContainerName,
318 + EscapePath(EnvTestFile1.wstring()),
319 + DebianImage.NameAndTag()));
320 + result.Verify({.Stderr = L"", .ExitCode = 0});
321 +
322 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_EQUALS=value=with=equals"));
323 + }
324 +
325 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_NameRoot)
326 + {
327 + auto result = RunWslc(std::format(L"container run --rm -u root {} sh -c \"id -un; id -u; id -g\"", DebianImage.NameAndTag()));
328 + result.Verify({.Stdout = L"root\n0\n0\n", .Stderr = L"", .ExitCode = 0});
329 + }
330 +
331 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_UidRoot)
332 + {
333 + auto result = RunWslc(std::format(L"container run --rm -u 0 {} id -u", DebianImage.NameAndTag()));
334 + result.Verify({.Stdout = L"0\n", .Stderr = L"", .ExitCode = 0});
335 + }
336 +
337 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_UidGidRoot)
338 + {
339 + auto result = RunWslc(std::format(L"container run --rm -u 0:0 {} sh -c \"id -u; id -g\"", DebianImage.NameAndTag()));
340 + result.Verify({.Stdout = L"0\n0\n", .Stderr = L"", .ExitCode = 0});
341 + }
342 +
343 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_UnknownUser_Fails)
344 + {
345 + auto result = RunWslc(std::format(L"container run --rm -u user_does_not_exist {} id -u", DebianImage.NameAndTag()));
346 + result.Verify(
347 + {.Stderr = L"unable to find user user_does_not_exist: no matching entries in passwd file\r\nError code: E_FAIL\r\n", .ExitCode = 1});
348 + }
349 +
350 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_UnknownGroup_Fails)
351 + {
352 + auto result = RunWslc(std::format(L"container run --rm -u root:badgid {} id -u", DebianImage.NameAndTag()));
353 + result.Verify({.Stderr = L"unable to find group badgid: no matching entries in group file\r\nError code: E_FAIL\r\n", .ExitCode = 1});
354 + }
355 +
356 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_NameGroupRoot)
357 + {
358 + auto result =
359 + RunWslc(std::format(L"container run --rm -u root:root {} sh -c \"id -un; id -u; id -g\"", DebianImage.NameAndTag()));
360 + result.Verify({.Stdout = L"root\n0\n0\n", .Stderr = L"", .ExitCode = 0});
361 + }
362 +
363 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_NonRootUser_Succeeds)
364 + {
365 + auto result = RunWslc(std::format(L"container run --rm -u nobody {} sh -c \"id -un; id -u; id -g\"", DebianImage.NameAndTag()));
366 + result.Verify({.Stdout = L"nobody\n65534\n65534\n", .Stderr = L"", .ExitCode = 0});
367 + }
368 +
369 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortMultipleMappings)
370 + {
371 + // Start a container with a simple server listening on a port
372 + // Map two host ports to the same container port
373 + auto result = RunWslc(std::format(
374 + L"container run -d --name {} -p {}:{} -p {}:{} {} {}",
375 + WslcContainerName,
376 + HostTestPort1,
377 + ContainerTestPort,
378 + HostTestPort2,
379 + ContainerTestPort,
380 + PythonImage.NameAndTag(),
381 + GetPythonHttpServerScript(ContainerTestPort)));
382 + result.Verify({.Stderr = L"", .ExitCode = 0});
383 +
384 + // From the host side, verify we can connect to both ports
385 + ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
386 + ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort2).c_str(), HTTP_STATUS_OK, true);
387 + }
388 +
389 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortAlreadyInUse)
390 + {
391 + // Start a container with a simple server listening on a port
392 + auto result1 = RunWslc(std::format(
393 + L"container run -d --name {} -p {}:{} {} {}",
394 + WslcContainerName,
395 + HostTestPort1,
396 + ContainerTestPort,
397 + PythonImage.NameAndTag(),
398 + GetPythonHttpServerScript(ContainerTestPort)));
399 + result1.Verify({.Stderr = L"", .ExitCode = 0});
400 +
401 + // Create a second container mapping the same host port to validate the full error message
402 + auto createResult =
403 + RunWslc(std::format(L"container create -p {}:{} {}", HostTestPort1, ContainerTestPort, DebianImage.NameAndTag()));
404 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
405 + auto containerId = createResult.GetStdoutOneLine();
406 +
407 + // Attempt to start — should fail with port conflict
408 + auto startResult = RunWslc(std::format(L"container start {}", containerId));
409 + startResult.Verify(
410 + {.Stderr = std::format(
411 + L"Port 127.0.0.1:{}/tcp is already in use, cannot start container {}\r\nError code: ERROR_ALREADY_EXISTS\r\n", HostTestPort1, containerId),
412 + .ExitCode = 1});
413 +
414 + // Clean up the created container
415 + RunWslc(std::format(L"container rm {}", containerId)).Verify({.Stderr = L"", .ExitCode = 0});
416 +
417 + // Verify 'container run' auto-cleans up on port conflict (no ghost container)
418 + auto runResult = RunWslc(std::format(
419 + L"container run --name {} -p {}:{} {}", WslcContainerName2, HostTestPort1, ContainerTestPort, DebianImage.NameAndTag()));
420 + runResult.Verify({.ExitCode = 1});
421 +
422 + VerifyContainerIsNotListed(WslcContainerName2);
423 + }
424 +
425 + // https://github.com/microsoft/WSL/issues/14433
426 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortEphemeral)
427 + {
428 + // Start a container with an ephemeral host port mapping (-p 8080 means host picks a random port)
429 + auto result = RunWslc(std::format(
430 + L"container run -d --name {} -p {} {} {}", WslcContainerName, ContainerTestPort, PythonImage.NameAndTag(), GetPythonHttpServerScript(ContainerTestPort)));
431 + result.Verify({.Stderr = L"", .ExitCode = 0});
432 +
433 + // Inspect the container to find the allocated host port
434 + auto inspectContainer = InspectContainer(WslcContainerName);
435 + auto portKey = std::to_string(ContainerTestPort) + "/tcp";
436 + VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
437 +
438 + auto portBindings = inspectContainer.Ports[portKey];
439 + VERIFY_ARE_EQUAL(1u, portBindings.size());
440 +
441 + auto hostPort = std::stoi(portBindings[0].HostPort);
442 + VERIFY_IS_TRUE(hostPort > 0);
443 +
444 + // Verify we can connect to the server on the ephemeral port
445 + ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", hostPort).c_str(), HTTP_STATUS_OK, true);
446 + }
447 +
448 + // https://github.com/microsoft/WSL/issues/14433
449 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortUdp_NotSupported)
450 + {
451 + auto result = RunWslc(std::format(L"container run -p 80:80/udp {}", DebianImage.NameAndTag()));
452 + result.Verify({.Stderr = L"Port mappings with specific host IPs or UDP protocol are not currently supported\r\nError code: ERROR_NOT_SUPPORTED\r\n", .ExitCode = 1});
453 + }
454 +
455 + // https://github.com/microsoft/WSL/issues/14433
456 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortHostIP_NotSupported)
457 + {
458 + auto result = RunWslc(std::format(L"container run -p 127.0.0.1:80:80 {}", DebianImage.NameAndTag()));
459 + result.Verify({.Stderr = L"Port mappings with specific host IPs or UDP protocol are not currently supported\r\nError code: ERROR_NOT_SUPPORTED\r\n", .ExitCode = 1});
460 + }
461 +
462 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_TCP)
463 + {
464 + // Start a container with a simple server listening on a port
465 + auto result = RunWslc(std::format(
466 + L"container run -d --name {} -p {}:{} {} {}",
467 + WslcContainerName,
468 + HostTestPort1,
469 + ContainerTestPort,
470 + PythonImage.NameAndTag(),
471 + GetPythonHttpServerScript(ContainerTestPort)));
472 + result.Verify({.Stderr = L"", .ExitCode = 0});
473 +
474 + // Verify we can connect to the server from the host side
475 + ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
476 +
477 + // Verify the port mapping is correct in the container inspect data
478 + auto inspectContainer = InspectContainer(WslcContainerName);
479 + auto portKey = std::to_string(ContainerTestPort) + "/tcp";
480 + VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
481 +
482 + auto portBindings = inspectContainer.Ports[portKey];
483 + VERIFY_ARE_EQUAL(1u, portBindings.size());
484 + VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
485 + VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
486 + }
487 +
488 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Interactive_TTY)
489 + {
490 + VerifyContainerIsNotListed(WslcContainerName);
491 +
492 + const auto& prompt = ">";
493 + auto session = RunWslcInteractive(
494 + std::format(L"container run -it -e PS1={} --name {} {} bash --norc", prompt, WslcContainerName, DebianImage.NameAndTag()));
495 + VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
496 +
497 + const auto& expectedPrompt = VT::BuildContainerPrompt(prompt);
498 + session.ExpectStdout(expectedPrompt);
499 +
500 + session.WriteLine("echo hello");
501 + session.ExpectCommandEcho("echo hello");
502 + session.ExpectStdout("hello\r\n");
503 + session.ExpectStdout(expectedPrompt);
504 +
505 + session.WriteLine("whoami");
506 + session.ExpectCommandEcho("whoami");
507 + session.ExpectStdout("root\r\n");
508 + session.ExpectStdout(expectedPrompt);
509 +
510 + auto exitCode = session.ExitAndVerifyNoErrors();
511 + VERIFY_ARE_EQUAL(0, exitCode);
512 + }
513 +
514 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Interactive_NoTTY)
515 + {
516 + VerifyContainerIsNotListed(WslcContainerName);
517 +
518 + auto session = RunWslcInteractive(std::format(L"container run -i --name {} {} cat", WslcContainerName, DebianImage.NameAndTag()));
519 + VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
520 +
521 + session.WriteLine("test line 1");
522 + session.ExpectStdout("test line 1\n");
523 + session.WriteLine("test line 2");
524 + session.ExpectStdout("test line 2\n");
525 +
526 + // Close stdin to signal EOF to cat
527 + session.CloseStdin();
528 +
529 + // Wait for cat to exit with code 0
530 + auto exitCode = session.Wait(10000);
531 + VERIFY_ARE_EQUAL(0, exitCode, L"Cat should exit with code 0 after receiving EOF");
532 + session.VerifyNoErrors();
533 + }
534 +
535 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs)
536 + {
537 + auto result = RunWslc(std::format(
538 + L"container run --rm --tmpfs /wslc-tmpfs {} sh -c \"echo -n 'tmpfs_test' > /wslc-tmpfs/data && cat "
539 + L"/wslc-tmpfs/data\"",
540 + DebianImage.NameAndTag()));
541 + result.Verify({.Stdout = L"tmpfs_test", .Stderr = L"", .ExitCode = 0});
542 + }
543 +
544 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_With_Options)
545 + {
546 + auto result = RunWslc(std::format(
547 + L"container run --rm --tmpfs /wslc-tmpfs:size=64k {} sh -c \"mount | grep -q ' on /wslc-tmpfs type tmpfs ' && echo "
548 + L"mounted\"",
549 + DebianImage.NameAndTag()));
550 + result.Verify({.Stdout = L"mounted\n", .Stderr = L"", .ExitCode = 0});
551 + }
552 +
553 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_Multiple_With_Options)
554 + {
555 + auto result = RunWslc(std::format(
556 + L"container run --rm --tmpfs /wslc-tmpfs1:size=64k --tmpfs /wslc-tmpfs2:size=128k {} sh -c \"mount | grep -q ' on "
557 + L"/wslc-tmpfs1 type tmpfs ' && mount | grep -q ' on /wslc-tmpfs2 type tmpfs ' && echo mounted\"",
558 + DebianImage.NameAndTag()));
559 + result.Verify({.Stdout = L"mounted\n", .Stderr = L"", .ExitCode = 0});
560 + }
561 +
562 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_RelativePath_Fails)
563 + {
564 + auto result = RunWslc(std::format(L"container run --rm --tmpfs wslc-tmpfs {}", DebianImage.NameAndTag()));
565 + result.Verify({.Stderr = L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
566 + }
567 +
568 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_EmptyDestination_Fails)
569 + {
570 + auto result = RunWslc(std::format(L"container run --rm --tmpfs :size=64k {}", DebianImage.NameAndTag()));
571 + result.Verify({.Stderr = L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
572 + }
573 +
574 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_WorkDir)
575 + {
576 + auto result = RunWslc(std::format(L"container run --rm --workdir /tmp {} pwd", DebianImage.NameAndTag()));
577 + result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
578 + }
579 +
580 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Hostname)
581 + {
582 + auto result = RunWslc(std::format(L"container run --rm --hostname my-test-host {} hostname", DebianImage.NameAndTag()));
583 + result.Verify({.Stdout = L"my-test-host\n", .Stderr = L"", .ExitCode = 0});
584 + }
585 +
586 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Domainname)
587 + {
588 + auto result = RunWslc(std::format(L"container run --rm --domainname my-test-domain {} dnsdomainname", DebianImage.NameAndTag()));
589 + result.Verify({.Stdout = L"my-test-domain\n", .Stderr = L"", .ExitCode = 0});
590 + }
591 +
592 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_DNS)
593 + {
594 + auto result =
595 + RunWslc(std::format(L"container run --rm --dns 1.1.1.1 --dns 8.8.8.8 {} cat /etc/resolv.conf", DebianImage.NameAndTag()));
596 + result.Verify({.Stderr = L"", .ExitCode = 0});
597 + VERIFY_IS_TRUE(result.Stdout->find(L"nameserver 1.1.1.1") != std::wstring::npos);
598 + VERIFY_IS_TRUE(result.Stdout->find(L"nameserver 8.8.8.8") != std::wstring::npos);
599 + }
600 +
601 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_DNSSearch)
602 + {
603 + auto result = RunWslc(std::format(
604 + L"container run --rm --dns-search example.com --dns-search test.local {} cat /etc/resolv.conf", DebianImage.NameAndTag()));
605 + result.Verify({.Stderr = L"", .ExitCode = 0});
606 + VERIFY_IS_TRUE(result.Stdout->find(L"search example.com test.local") != std::wstring::npos);
607 + }
608 +
609 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_DNSOption)
610 + {
611 + auto result = RunWslc(std::format(
612 + L"container run --rm --dns-option ndots:5 --dns-option timeout:3 {} cat /etc/resolv.conf", DebianImage.NameAndTag()));
613 + result.Verify({.Stderr = L"", .ExitCode = 0});
614 + VERIFY_IS_TRUE(result.Stdout->find(L"options ndots:5 timeout:3") != std::wstring::npos);
615 + }
616 +
617 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_Success)
618 + {
619 + // Create a named volume
620 + auto result = RunWslc(std::format(L"volume create {}", WslcVolumeName));
621 + result.Verify({.Stderr = L"", .ExitCode = 0});
622 +
623 + // Create a container with --rm that uses the named volume and writes a file to it
624 + result = RunWslc(std::format(
625 + L"container run --rm --volume {}:/data {} sh -c \"echo -n 'WSLC Named Volume Test' > /data/test.txt\"",
626 + WslcVolumeName,
627 + DebianImage.NameAndTag()));
628 + result.Verify({.Stderr = L"", .ExitCode = 0});
629 +
630 + // Create another container that mounts the same named volume and verify the file content
631 + result = RunWslc(std::format(L"container run --rm --volume {}:/data {} cat /data/test.txt", WslcVolumeName, DebianImage.NameAndTag()));
632 + result.Verify({.Stdout = L"WSLC Named Volume Test", .Stderr = L"", .ExitCode = 0});
633 + }
634 +
635 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_NotFound_Fail)
636 + {
637 + auto result = RunWslc(std::format(
638 + L"container run --rm --volume {}:/data {} sh -c \"echo -n 'WSLC Named Volume Test' > /data/test.txt\"",
639 + WslcVolumeName,
640 + DebianImage.NameAndTag()));
641 + result.Verify({.Stderr = std::format(L"Volume not found: '{}'\r\nError code: WSLC_E_VOLUME_NOT_FOUND\r\n", WslcVolumeName), .ExitCode = 1});
642 + }
643 +
644 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_WithLabel_Success)
645 + {
646 + auto result = RunWslc(std::format(
647 + L"container run --name {} --label A=1 --label B=2 {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
648 + result.Verify({.Stdout = L"hello\n", .Stderr = L"", .ExitCode = 0});
649 +
650 + auto inspect = InspectContainer(WslcContainerName);
651 + VERIFY_ARE_EQUAL("1", inspect.Labels["A"]);
652 + VERIFY_ARE_EQUAL("2", inspect.Labels["B"]);
653 + }
654 +
655 +private:
656 + // Test container name
657 + const std::wstring WslcContainerName = L"wslc-test-container";
658 + const std::wstring WslcContainerName2 = L"wslc-test-container-2";
659 +
660 + // Test environment variables
661 + const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
662 + const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
663 + const std::wstring HostEnvVariableValue = L"wslc-host-env-value";
664 + const std::wstring HostEnvVariableValue2 = L"wslc-host-env-value2";
665 +
666 + // Test images
667 + const TestImage& DebianImage = DebianTestImage();
668 + const TestImage& PythonImage = PythonTestImage();
669 +
670 + // Test environment variable files
671 + std::filesystem::path EnvTestFile1;
672 + std::filesystem::path EnvTestFile2;
673 +
674 + // Test ports
675 + const uint16_t ContainerTestPort = 8080;
676 + const uint16_t HostTestPort1 = 1234;
677 + const uint16_t HostTestPort2 = 1235;
678 +
679 + // Test named volume
680 + const std::wstring WslcVolumeName = L"wslc-test-volume";
681 +
682 + std::wstring GetHelpMessage() const
683 + {
684 + std::wstringstream output;
685 + output << GetWslcHeader() //
686 + << GetDescription() //
687 + << GetUsage() //
688 + << GetAvailableCommands() //
689 + << GetAvailableOptions();
690 + return output.str();
691 + }
692 +
693 + std::wstring GetDescription() const
694 + {
695 + return L"Runs a container. By default, the container is started in the foreground; use --detach to run in the "
696 + L"background.\r\n\r\n";
697 + }
698 +
699 + std::wstring GetUsage() const
700 + {
701 + return L"Usage: wslc container run [<options>] <image> [<command>] [<arguments>...]\r\n\r\n";
702 + }
703 +
704 + std::wstring GetAvailableCommands() const
705 + {
706 + std::wstringstream commands;
707 + commands << L"The following arguments are available:\r\n"
708 + << L" image Image name\r\n"
709 + << L" command The command to run\r\n"
710 + << L" arguments Arguments to pass to container's init process\r\n"
711 + << L"\r\n";
712 + return commands.str();
713 + }
714 +
715 + std::wstring GetAvailableOptions() const
716 + {
717 + std::wstringstream options;
718 + options << L"The following options are available:\r\n"
719 + << L" -d,--detach Run container in detached mode\r\n"
720 + << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
721 + << L" --dns-option Set DNS options\r\n"
722 + << L" --dns-search Set DNS search domains\r\n"
723 + << L" --domainname Container domain name\r\n"
724 + << L" --entrypoint Specifies the container init process executable\r\n"
725 + << L" -e,--env Key=Value pairs for environment variables\r\n"
726 + << L" --env-file File containing key=value pairs of env variables\r\n"
727 + << L" -h,--hostname Container host name\r\n"
728 + << L" -i,--interactive Attach to stdin and keep it open\r\n"
729 + << L" -l,--label Set metadata on an object\r\n"
730 + << L" --name Name of the container\r\n"
731 + << L" -p,--publish Publish a port from a container to host\r\n"
732 + << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
733 + << L" --rm Remove the container after it stops\r\n"
734 + << L" --session Specify the session to use\r\n"
735 + << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
736 + << L" -t,--tty Open a TTY with the container process.\r\n"
737 + << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
738 + << L" -v,--volume Bind mount a volume to the container\r\n"
739 + << L" -w,--workdir Working directory inside the container\r\n"
740 + << L" -?,--help Shows help about the selected command\r\n"
741 + << L"\r\n";
742 + return options.str();
743 + }
744 +};
745 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp new
+304
@@ -0,0 +1,304 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerStopTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21 +
22 +class WSLCE2EContainerStopTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EContainerStopTests)
25 +
26 + TEST_CLASS_SETUP(ClassSetup)
27 + {
28 + EnsureImageIsLoaded(DebianImage);
29 + return true;
30 + }
31 +
32 + TEST_CLASS_CLEANUP(ClassCleanup)
33 + {
34 + EnsureContainerDoesNotExist(WslcContainerName);
35 + EnsureContainerDoesNotExist(WslcContainerName2);
36 + EnsureImageIsDeleted(DebianImage);
37 + return true;
38 + }
39 +
40 + TEST_METHOD_SETUP(TestMethodSetup)
41 + {
42 + EnsureContainerDoesNotExist(WslcContainerName);
43 + EnsureContainerDoesNotExist(WslcContainerName2);
44 + return true;
45 + }
46 +
47 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_HelpCommand)
48 + {
49 + auto result = RunWslc(L"container stop --help");
50 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
51 + }
52 +
53 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_InvalidSignal)
54 + {
55 + auto result = RunWslc(std::format(L"container run --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
56 + result.Verify({.Stderr = L"", .ExitCode = 0});
57 +
58 + {
59 + result = RunWslc(std::format(L"container stop {} -s 0 -t 0", WslcContainerName));
60 + result.Verify({.Stderr = L"Invalid signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
61 + }
62 +
63 + {
64 + result = RunWslc(std::format(L"container stop {} -s 32 -t 0", WslcContainerName));
65 + result.Verify({.Stderr = L"Invalid signal value: 32 is out of valid range (1-31).\r\n", .ExitCode = 1});
66 + }
67 + }
68 +
69 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_KillsRunningContainer)
70 + {
71 + // Run a container in the background
72 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
73 + result.Verify({.Stderr = L"", .ExitCode = 0});
74 + auto containerId = result.GetStdoutOneLine();
75 + VERIFY_IS_FALSE(containerId.empty());
76 +
77 + // Verify container is running
78 + VerifyContainerIsListed(containerId, L"running");
79 +
80 + // Stop the container
81 + result = RunWslc(std::format(L"container stop {} -t 0", containerId));
82 + result.Verify({.Stderr = L"", .ExitCode = 0});
83 +
84 + // Verify the container is no longer running
85 + VerifyContainerIsListed(containerId, L"exited");
86 + }
87 +
88 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_ByName)
89 + {
90 + // Run a container in the background
91 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
92 + result.Verify({.Stderr = L"", .ExitCode = 0});
93 + const auto containerId = result.GetStdoutOneLine();
94 + VERIFY_IS_FALSE(containerId.empty());
95 +
96 + // Verify container is running
97 + VerifyContainerIsListed(containerId, L"running");
98 +
99 + // Stop by container name
100 + result = RunWslc(std::format(L"container stop {} -t 0", WslcContainerName));
101 + result.Verify({.Stderr = L"", .ExitCode = 0});
102 +
103 + // Verify container is no longer running
104 + VerifyContainerIsListed(containerId, L"exited");
105 + }
106 +
107 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_AlreadyStopped)
108 + {
109 + // Run a container in the background
110 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
111 + result.Verify({.Stderr = L"", .ExitCode = 0});
112 + auto containerId = result.GetStdoutOneLine();
113 + VERIFY_IS_FALSE(containerId.empty());
114 +
115 + // Stop the container
116 + result = RunWslc(std::format(L"container stop {} -t 0", containerId));
117 + result.Verify({.Stderr = L"", .ExitCode = 0});
118 + VerifyContainerIsListed(containerId, L"exited");
119 +
120 + // Stop again - should succeed without error
121 + result = RunWslc(std::format(L"container stop {} -t 0", containerId));
122 + result.Verify({.Stderr = L"", .ExitCode = 0});
123 + VerifyContainerIsListed(containerId, L"exited");
124 + }
125 +
126 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_NotFound)
127 + {
128 + VerifyContainerIsNotListed(WslcContainerName);
129 +
130 + auto result = RunWslc(std::format(L"container stop {} -t 0", WslcContainerName));
131 + result.Verify(
132 + {.Stderr = std::format(L"Container '{}' not found.\r\nError code: WSLC_E_CONTAINER_NOT_FOUND\r\n", WslcContainerName),
133 + .ExitCode = 1});
134 + }
135 +
136 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_TargetedContainerOnly)
137 + {
138 + // Run first container in background
139 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
140 + result.Verify({.Stderr = L"", .ExitCode = 0});
141 + const auto firstContainerId = result.GetStdoutOneLine();
142 + VERIFY_IS_FALSE(firstContainerId.empty());
143 +
144 + // Run second container in background
145 + result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName2, DebianImage.NameAndTag()));
146 + result.Verify({.Stderr = L"", .ExitCode = 0});
147 + const auto secondContainerId = result.GetStdoutOneLine();
148 + VERIFY_IS_FALSE(secondContainerId.empty());
149 +
150 + // Verify both are running
151 + VerifyContainerIsListed(firstContainerId, L"running");
152 + VerifyContainerIsListed(secondContainerId, L"running");
153 +
154 + // Stop only the first container
155 + result = RunWslc(std::format(L"container stop {} -t 0", firstContainerId));
156 + result.Verify({.Stderr = L"", .ExitCode = 0});
157 +
158 + // Verify first exited, second still running
159 + VerifyContainerIsListed(firstContainerId, L"exited");
160 + VerifyContainerIsListed(secondContainerId, L"running");
161 + }
162 +
163 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_SignalByName)
164 + {
165 + // Run a container in the background
166 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
167 + result.Verify({.Stderr = L"", .ExitCode = 0});
168 + const auto containerId = result.GetStdoutOneLine();
169 + VERIFY_IS_FALSE(containerId.empty());
170 +
171 + // Verify container is running
172 + VerifyContainerIsListed(containerId, L"running");
173 +
174 + // Stop the container using signal name
175 + result = RunWslc(std::format(L"container stop {} -s SIGKILL -t 0", containerId));
176 + result.Verify({.Stderr = L"", .ExitCode = 0});
177 +
178 + // Verify the container is no longer running
179 + VerifyContainerIsListed(containerId, L"exited");
180 + }
181 +
182 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_InvalidSignalName)
183 + {
184 + // Run a container in the background
185 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
186 + result.Verify({.Stderr = L"", .ExitCode = 0});
187 + const auto containerId = result.GetStdoutOneLine();
188 + VERIFY_IS_FALSE(containerId.empty());
189 +
190 + // Verify container is running
191 + VerifyContainerIsListed(containerId, L"running");
192 +
193 + // Try to stop with an invalid signal name
194 + result = RunWslc(std::format(L"container stop {} -s SIGINVALID -t 0", containerId));
195 + result.Verify({.Stderr = L"Invalid signal value: SIGINVALID is not a recognized signal name or number (Example: SIGKILL, kill, or 9).\r\n", .ExitCode = 1});
196 +
197 + // Verify container is still running after failed stop request
198 + VerifyContainerIsListed(containerId, L"running");
199 + }
200 +
201 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_InvalidTimeout)
202 + {
203 + // Run a container in the background
204 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
205 + result.Verify({.Stderr = L"", .ExitCode = 0});
206 + const auto containerId = result.GetStdoutOneLine();
207 + VERIFY_IS_FALSE(containerId.empty());
208 +
209 + // Verify container is running
210 + VerifyContainerIsListed(containerId, L"running");
211 +
212 + {
213 + // Invalid integer
214 + result = RunWslc(std::format(L"container stop {} -t abc", containerId));
215 + result.Verify({.Stderr = L"Invalid time argument value: abc\r\n", .ExitCode = 1});
216 +
217 + // Should still be running after failed stop
218 + VerifyContainerIsListed(containerId, L"running");
219 + }
220 +
221 + {
222 + // Another invalid integer shape
223 + result = RunWslc(std::format(L"container stop {} -t 1.5", containerId));
224 + result.Verify({.Stderr = L"Invalid time argument value: 1.5\r\n", .ExitCode = 1});
225 +
226 + // Should still be running after failed stop
227 + VerifyContainerIsListed(containerId, L"running");
228 + }
229 +
230 + {
231 + // Invalid integer prefixed
232 + result = RunWslc(std::format(L"container stop {} -t 9abc", containerId));
233 + result.Verify({.Stderr = L"Invalid time argument value: 9abc\r\n", .ExitCode = 1});
234 +
235 + // Should still be running after failed stop
236 + VerifyContainerIsListed(containerId, L"running");
237 + }
238 + }
239 +
240 + WSLC_TEST_METHOD(WSLCE2E_Container_Stop_ValidTimeoutNegativeOne)
241 + {
242 + // Run a container in the background
243 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
244 + result.Verify({.Stderr = L"", .ExitCode = 0});
245 + const auto containerId = result.GetStdoutOneLine();
246 + VERIFY_IS_FALSE(containerId.empty());
247 +
248 + // Verify container is running
249 + VerifyContainerIsListed(containerId, L"running");
250 +
251 + // -1 is a valid timeout value
252 + result = RunWslc(std::format(L"container stop {} -t -1", containerId));
253 + result.Verify({.Stderr = L"", .ExitCode = 0});
254 +
255 + // Verify the container is no longer running
256 + VerifyContainerIsListed(containerId, L"exited");
257 + }
258 +
259 +private:
260 + const std::wstring WslcContainerName = L"wslc-test-container";
261 + const std::wstring WslcContainerName2 = L"wslc-test-container-2";
262 + const TestImage& DebianImage = DebianTestImage();
263 +
264 + std::wstring GetHelpMessage() const
265 + {
266 + std::wstringstream output;
267 + output << GetWslcHeader() //
268 + << GetDescription() //
269 + << GetUsage() //
270 + << GetAvailableCommands() //
271 + << GetAvailableOptions();
272 + return output.str();
273 + }
274 +
275 + std::wstring GetDescription() const
276 + {
277 + return Localization::WSLCCLI_ContainerStopLongDesc() + L"\r\n\r\n";
278 + }
279 +
280 + std::wstring GetUsage() const
281 + {
282 + return L"Usage: wslc container stop [<options>] [<container-id>]\r\n\r\n";
283 + }
284 +
285 + std::wstring GetAvailableCommands() const
286 + {
287 + std::wstringstream commands;
288 + commands << L"The following arguments are available:\r\n" << L" container-id Container ID\r\n" << L"\r\n";
289 + return commands.str();
290 + }
291 +
292 + std::wstring GetAvailableOptions() const
293 + {
294 + std::wstringstream options;
295 + options << L"The following options are available:\r\n"
296 + << L" --session Specify the session to use\r\n"
297 + << L" -s,--signal Signal to send (default: SIGTERM)\r\n"
298 + << L" -t,--time Time in seconds to wait before executing (default 5)\r\n"
299 + << L" -?,--help Shows help about the selected command\r\n"
300 + << L"\r\n";
301 + return options.str();
302 + }
303 +};
304 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EContainerTests.cpp new
+109
@@ -0,0 +1,109 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCCLITestHelpers.h"
17 +#include "WSLCExecutor.h"
18 +#include "Argument.h"
19 +
20 +namespace WSLCE2ETests {
21 +using namespace wsl::shared;
22 +
23 +class WSLCE2EContainerTests
24 +{
25 + WSLC_TEST_CLASS(WSLCE2EContainerTests)
26 +
27 + TEST_CLASS_SETUP(TestClassSetup)
28 + {
29 + return true;
30 + }
31 +
32 + TEST_CLASS_CLEANUP(TestClassCleanup)
33 + {
34 + return true;
35 + }
36 +
37 + WSLC_TEST_METHOD(WSLCE2E_Container_HelpCommand)
38 + {
39 + RunWslc(L"container --help").Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
40 + }
41 +
42 + WSLC_TEST_METHOD(WSLCE2E_Container_InvalidCommand_DisplaysErrorMessage)
43 + {
44 + RunWslc(L"container INVALID_CMD").Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
45 + }
46 +
47 +private:
48 + std::wstring GetHelpMessage() const
49 + {
50 + std::wstringstream output;
51 + output << GetWslcHeader() //
52 + << GetDescription() //
53 + << GetUsage() //
54 + << GetAvailableCommands() //
55 + << GetAvailableOptions();
56 + return output.str();
57 + }
58 +
59 + std::wstring GetDescription() const
60 + {
61 + return Localization::WSLCCLI_ContainerCommandLongDesc() + L"\r\n\r\n";
62 + }
63 +
64 + std::wstring GetUsage() const
65 + {
66 + return L"Usage: wslc container [<command>] [<options>]\r\n\r\n";
67 + }
68 +
69 + std::wstring GetAvailableCommands() const
70 + {
71 + std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
72 + {L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
73 + {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
74 + {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
75 + {L"inspect", Localization::WSLCCLI_ContainerInspectDesc()},
76 + {L"kill", Localization::WSLCCLI_ContainerKillDesc()},
77 + {L"logs", Localization::WSLCCLI_ContainerLogsDesc()},
78 + {L"list", Localization::WSLCCLI_ContainerListDesc()},
79 + {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()},
80 + {L"run", Localization::WSLCCLI_ContainerRunDesc()},
81 + {L"start", Localization::WSLCCLI_ContainerStartDesc()},
82 + {L"stop", Localization::WSLCCLI_ContainerStopDesc()},
83 + };
84 +
85 + size_t maxLen = 0;
86 + for (const auto& [name, _] : entries)
87 + {
88 + maxLen = (std::max)(maxLen, name.size());
89 + }
90 +
91 + std::wstringstream commands;
92 + commands << Localization::WSLCCLI_AvailableSubcommands() << L"\r\n";
93 + for (const auto& [name, desc] : entries)
94 + {
95 + commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
96 + }
97 + commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n\r\n";
98 + return commands.str();
99 + }
100 +
101 + std::wstring GetAvailableOptions() const
102 + {
103 + std::wstringstream options;
104 + options << L"The following options are available:\r\n" //
105 + << L" -?,--help Shows help about the selected command\r\n\r\n";
106 + return options.str();
107 + }
108 +};
109 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp new
+582
@@ -0,0 +1,582 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EGlobalTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCCLITestHelpers.h"
17 +#include "WSLCExecutor.h"
18 +#include "WSLCE2EHelpers.h"
19 +#include "WSLCSessionDefaults.h"
20 +#include "Argument.h"
21 +
22 +using namespace WEX::Logging;
23 +
24 +namespace WSLCE2ETests {
25 +using namespace wsl::shared;
26 +
27 +namespace {
28 +
29 + // Returns the expected default session name for the current user (e.g. "wslc-cli-admin-benhill").
30 + std::wstring GetExpectedDefaultSessionName(bool elevated)
31 + {
32 + auto baseName = elevated ? wsl::windows::wslc::DefaultAdminSessionName : wsl::windows::wslc::DefaultSessionName;
33 +
34 + wchar_t username[256 + 1] = {};
35 + DWORD usernameLen = ARRAYSIZE(username);
36 + THROW_IF_WIN32_BOOL_FALSE(GetUserNameW(username, &usernameLen));
37 +
38 + return std::format(L"{}-{}", baseName, username);
39 + }
40 +
41 +} // namespace
42 +
43 +class WSLCE2EGlobalTests
44 +{
45 + WSLC_TEST_CLASS(WSLCE2EGlobalTests)
46 +
47 + wil::unique_couninitialize_call m_coinit = wil::CoInitializeEx();
48 +
49 + TEST_CLASS_SETUP(TestClassSetup)
50 + {
51 + return true;
52 + }
53 +
54 + TEST_CLASS_CLEANUP(TestClassCleanup)
55 + {
56 + return true;
57 + }
58 +
59 + WSLC_TEST_METHOD(WSLCE2E_HelpCommand)
60 + {
61 + RunWslcAndVerify(L"--help", {.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
62 + }
63 +
64 + WSLC_TEST_METHOD(WSLCE2E_InvalidCommand_DisplaysErrorMessage)
65 + {
66 + RunWslcAndVerify(L"INVALID_CMD", {.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
67 + }
68 +
69 + WSLC_TEST_METHOD(WSLCE2E_VersionCommand)
70 + {
71 + RunWslcAndVerify(L"version", {.Stdout = GetVersionMessage(), .Stderr = L"", .ExitCode = 0});
72 + }
73 +
74 + WSLC_TEST_METHOD(WSLCE2E_VersionFlag)
75 + {
76 + RunWslcAndVerify(L"--version", {.Stdout = GetVersionMessage(), .Stderr = L"", .ExitCode = 0});
77 + }
78 +
79 + WSLC_TEST_METHOD(WSLCE2E_Session_DefaultElevated)
80 + {
81 + // Run container list to create the default elevated session
82 + auto result = RunWslc(L"container list", ElevationType::Elevated);
83 + result.Verify({.Stderr = L"", .ExitCode = 0});
84 +
85 + // Verify session list shows the admin session name
86 + result = RunWslc(L"session list", ElevationType::Elevated);
87 + result.Verify({.Stderr = L"", .ExitCode = 0});
88 +
89 + VERIFY_IS_TRUE(result.Stdout.has_value());
90 + auto adminName = GetExpectedDefaultSessionName(true);
91 + VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
92 + }
93 +
94 + WSLC_TEST_METHOD(WSLCE2E_Session_DefaultNonElevated)
95 + {
96 + // Run container list non-elevated to create the default non-elevated session
97 + auto result = RunWslc(L"container list", ElevationType::NonElevated);
98 + result.Verify({.Stderr = L"", .ExitCode = 0});
99 +
100 + // Verify session list shows the non-admin session name
101 + result = RunWslc(L"session list", ElevationType::NonElevated);
102 + result.Verify({.Stderr = L"", .ExitCode = 0});
103 +
104 + VERIFY_IS_TRUE(result.Stdout.has_value());
105 +
106 + // The "\r\n" after session name is important to differentiate it from the admin session.
107 + auto nonAdminName = GetExpectedDefaultSessionName(false);
108 + VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
109 + }
110 +
111 + WSLC_TEST_METHOD(WSLCE2E_Session_NonElevatedCannotAccessAdminSession)
112 + {
113 + // First ensure admin session is created by running container list.
114 + auto result = RunWslc(L"container list", ElevationType::Elevated);
115 + result.Verify({.Stderr = L"", .ExitCode = 0});
116 +
117 + // Try to explicitly target the admin session from non-elevated process
118 + auto adminName = GetExpectedDefaultSessionName(true);
119 + result = RunWslc(std::format(L"container list --session {}", adminName), ElevationType::NonElevated);
120 +
121 + // Should fail with access denied.
122 + result.Verify({.Stderr = L"The requested operation requires elevation. \r\nError code: ERROR_ELEVATION_REQUIRED\r\n", .ExitCode = 1});
123 + }
124 +
125 + WSLC_TEST_METHOD(WSLCE2E_Session_ElevatedCanAccessNonElevatedSession)
126 + {
127 + // First ensure non-elevated session is created by running container list.
128 + auto result = RunWslc(L"container list", ElevationType::NonElevated);
129 + result.Verify({.Stderr = L"", .ExitCode = 0});
130 +
131 + // Elevated user should be able to explicitly target the non-admin session
132 + auto nonAdminName = GetExpectedDefaultSessionName(false);
133 + result = RunWslc(std::format(L"container list --session {}", nonAdminName), ElevationType::Elevated);
134 +
135 + // This should work - elevated users can access non-elevated sessions
136 + result.Verify({.Stderr = L"", .ExitCode = 0});
137 + }
138 +
139 + WSLC_TEST_METHOD(WSLCE2E_Session_CreateMixedElevation_Fails)
140 + {
141 + EnsureSessionIsTerminated(GetExpectedDefaultSessionName(false));
142 + EnsureSessionIsTerminated(GetExpectedDefaultSessionName(true));
143 +
144 + // Ensure elevated cannot create the non-elevated session.
145 + auto nonAdminName = GetExpectedDefaultSessionName(false);
146 + auto adminName = GetExpectedDefaultSessionName(true);
147 + auto result = RunWslc(std::format(L"container list --session {}", nonAdminName), ElevationType::Elevated);
148 + result.Verify({.Stderr = L"Element not found. \r\nError code: ERROR_NOT_FOUND\r\n", .ExitCode = 1});
149 +
150 + // Ensure non-elevated cannot create the elevated session.
151 + result = RunWslc(std::format(L"container list --session {}", adminName), ElevationType::NonElevated);
152 + result.Verify({.Stderr = L"Element not found. \r\nError code: ERROR_NOT_FOUND\r\n", .ExitCode = 1});
153 + }
154 +
155 + // Regression test for session name squatting vulnerability.
156 + //
157 + // Validates that a process cannot create a session with the reserved default
158 + // session names ("wslc-cli" or "wslc-cli-admin") via the COM API. These names
159 + // are assigned server-side when the client passes null Settings to CreateSession,
160 + // preventing a malicious process from squatting on the name and blocking
161 + // legitimate wslc.exe clients.
162 + WSLC_TEST_METHOD(WSLCE2E_Session_NameSquatting_ElevatedCannotBlockNonElevated)
163 + {
164 + // Ensure no existing sessions with default names.
165 + EnsureSessionIsTerminated(wsl::windows::wslc::DefaultSessionName);
166 + EnsureSessionIsTerminated(wsl::windows::wslc::DefaultAdminSessionName);
167 +
168 + // Attack: attempt to create a session with the reserved non-admin default
169 + // name directly through the COM API from this elevated process.
170 + // The service should reject this because reserved default session names
171 + // cannot be explicitly created.
172 + {
173 + wil::com_ptr<IWSLCSessionManager> sessionManager;
174 + VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
175 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
176 +
177 + WSLCSessionSettings settings{};
178 + settings.DisplayName = wsl::windows::wslc::DefaultSessionName;
179 + settings.StoragePath = L"C:\\dummy";
180 + settings.CpuCount = 4;
181 + settings.MemoryMb = 2048;
182 + settings.BootTimeoutMs = 30000;
183 + settings.MaximumStorageSizeMb = 4096;
184 +
185 + wil::com_ptr<IWSLCSession> session;
186 + HRESULT hr = sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session);
187 + VERIFY_ARE_EQUAL(hr, WSLC_E_SESSION_RESERVED);
188 + }
189 +
190 + // Also verify that the admin reserved name is rejected.
191 + {
192 + wil::com_ptr<IWSLCSessionManager> sessionManager;
193 + VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
194 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
195 +
196 + WSLCSessionSettings settings{};
197 + settings.DisplayName = wsl::windows::wslc::DefaultAdminSessionName;
198 + settings.StoragePath = L"C:\\dummy";
199 + settings.CpuCount = 4;
200 + settings.MemoryMb = 2048;
201 + settings.BootTimeoutMs = 30000;
202 + settings.MaximumStorageSizeMb = 4096;
203 +
204 + wil::com_ptr<IWSLCSession> session;
205 + HRESULT hr = sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session);
206 + VERIFY_ARE_EQUAL(hr, WSLC_E_SESSION_RESERVED);
207 + }
208 +
209 + // Non-elevated wslc.exe should still be able to create and use its default
210 + // session (which now passes null Settings, resolved entirely server-side).
211 + auto result = RunWslc(L"container list", ElevationType::NonElevated);
212 + result.Verify({.Stderr = L"", .ExitCode = S_OK});
213 +
214 + // Verify that case variations of reserved names are also rejected,
215 + // preventing bypass on case-insensitive filesystems (NTFS).
216 + {
217 + wil::com_ptr<IWSLCSessionManager> sessionManager;
218 + VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
219 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
220 +
221 + WSLCSessionSettings settings{};
222 + settings.DisplayName = L"WSLC-CLI";
223 + settings.StoragePath = L"C:\\dummy";
224 + settings.CpuCount = 4;
225 + settings.MemoryMb = 2048;
226 + settings.BootTimeoutMs = 30000;
227 + settings.MaximumStorageSizeMb = 4096;
228 +
229 + wil::com_ptr<IWSLCSession> session;
230 + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_SESSION_RESERVED);
231 +
232 + settings.DisplayName = L"Wslc-Cli-Admin";
233 + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_SESSION_RESERVED);
234 + }
235 + }
236 +
237 + WSLC_TEST_METHOD(WSLCE2E_Session_Terminate_Implicit)
238 + {
239 + auto adminName = GetExpectedDefaultSessionName(true);
240 + auto nonAdminName = GetExpectedDefaultSessionName(false);
241 +
242 + // Run container list to create the default session if it does not already exist
243 + auto result = RunWslc(L"container list");
244 + result.Verify({.Stderr = L"", .ExitCode = 0});
245 +
246 + // Verify session list shows the admin session name
247 + result = RunWslc(L"session list");
248 + result.Verify({.Stderr = L"", .ExitCode = 0});
249 + VERIFY_IS_TRUE(result.Stdout.has_value());
250 + VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
251 +
252 + // Terminate the session
253 + result = RunWslc(L"session terminate");
254 + result.Verify({.Stderr = L"", .ExitCode = 0});
255 +
256 + // Verify session no longer shows up
257 + result = RunWslc(L"session list");
258 + result.Verify({.Stderr = L"", .ExitCode = 0});
259 + VERIFY_IS_TRUE(result.Stdout.has_value());
260 + VERIFY_IS_FALSE(result.Stdout->find(adminName) != std::wstring::npos);
261 +
262 + // Repeat test for non-elevated session.
263 +
264 + // Run container list to create the default session if it does not already exist
265 + result = RunWslc(L"container list", ElevationType::NonElevated);
266 + result.Verify({.Stderr = L"", .ExitCode = 0});
267 +
268 + // Verify session list shows the non-elevated session name
269 + result = RunWslc(L"session list");
270 + result.Verify({.Stderr = L"", .ExitCode = 0});
271 + VERIFY_IS_TRUE(result.Stdout.has_value());
272 + VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
273 +
274 + // Terminate the session
275 + result = RunWslc(L"session terminate", ElevationType::NonElevated);
276 + result.Verify({.Stderr = L"", .ExitCode = 0});
277 +
278 + // Verify session no longer shows up
279 + result = RunWslc(L"session list");
280 + result.Verify({.Stderr = L"", .ExitCode = 0});
281 + VERIFY_IS_TRUE(result.Stdout.has_value());
282 + VERIFY_IS_FALSE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
283 + }
284 +
285 + WSLC_TEST_METHOD(WSLCE2E_Session_Terminate_Explicit)
286 + {
287 + auto adminName = GetExpectedDefaultSessionName(true);
288 + auto nonAdminName = GetExpectedDefaultSessionName(false);
289 +
290 + // Run container list to create the default session if it does not already exist
291 + auto result = RunWslc(L"container list");
292 + result.Verify({.Stderr = L"", .ExitCode = 0});
293 +
294 + // Verify session list shows the admin session name
295 + result = RunWslc(L"session list");
296 + result.Verify({.Stderr = L"", .ExitCode = 0});
297 + VERIFY_IS_TRUE(result.Stdout.has_value());
298 + VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
299 +
300 + // Terminate the session
301 + result = RunWslc(std::format(L"session terminate {}", adminName));
302 + result.Verify({.Stderr = L"", .ExitCode = 0});
303 +
304 + // Verify session no longer shows up
305 + result = RunWslc(L"session list");
306 + result.Verify({.Stderr = L"", .ExitCode = 0});
307 + VERIFY_IS_TRUE(result.Stdout.has_value());
308 + VERIFY_IS_FALSE(result.Stdout->find(adminName) != std::wstring::npos);
309 +
310 + // Repeat test for non-elevated session.
311 +
312 + // Run container list to create the default session if it does not already exist
313 + result = RunWslc(L"container list", ElevationType::NonElevated);
314 + result.Verify({.Stderr = L"", .ExitCode = 0});
315 +
316 + // Verify session list shows the non-elevated session name
317 + result = RunWslc(L"session list");
318 + result.Verify({.Stderr = L"", .ExitCode = 0});
319 + VERIFY_IS_TRUE(result.Stdout.has_value());
320 + VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
321 +
322 + // Terminate the session
323 + result = RunWslc(std::format(L"session terminate {}", nonAdminName), ElevationType::NonElevated);
324 + result.Verify({.Stderr = L"", .ExitCode = 0});
325 +
326 + // Verify session no longer shows up
327 + result = RunWslc(L"session list");
328 + result.Verify({.Stderr = L"", .ExitCode = 0});
329 + VERIFY_IS_TRUE(result.Stdout.has_value());
330 + VERIFY_IS_FALSE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
331 + }
332 +
333 + WSLC_TEST_METHOD(WSLCE2E_Session_Terminate_MixedElevation)
334 + {
335 + auto adminName = GetExpectedDefaultSessionName(true);
336 + auto nonAdminName = GetExpectedDefaultSessionName(false);
337 +
338 + // Run container list to create the default sessions if they do not already exist.
339 + auto result = RunWslc(L"container list", ElevationType::Elevated);
340 + result.Verify({.Stderr = L"", .ExitCode = 0});
341 + result = RunWslc(L"container list", ElevationType::NonElevated);
342 + result.Verify({.Stderr = L"", .ExitCode = 0});
343 +
344 + // Verify session list shows both sessions.
345 + result = RunWslc(L"session list");
346 + result.Verify({.Stderr = L"", .ExitCode = 0});
347 + VERIFY_IS_TRUE(result.Stdout.has_value());
348 + VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
349 + VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
350 +
351 + // Attempt to terminate the admin session from the non-elevated process and fail.
352 + result = RunWslc(std::format(L"session terminate {}", adminName), ElevationType::NonElevated);
353 + result.Verify({.Stderr = L"The requested operation requires elevation. \r\nError code: ERROR_ELEVATION_REQUIRED\r\n", .ExitCode = 1});
354 +
355 + // Terminate the non-elevated session from the elevated process.
356 + result = RunWslc(std::format(L"session terminate {}", nonAdminName), ElevationType::Elevated);
357 + result.Verify({.Stderr = L"", .ExitCode = 0});
358 +
359 + // Verify non-elevated session no longer shows up
360 + result = RunWslc(L"session list");
361 + result.Verify({.Stderr = L"", .ExitCode = 0});
362 + VERIFY_IS_TRUE(result.Stdout.has_value());
363 + VERIFY_IS_FALSE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
364 + }
365 +
366 + WSLC_TEST_METHOD(WSLCE2E_Session_Targeting)
367 + {
368 + // Generate a unique session name to avoid conflicts with previous runs or concurrent tests.
369 + // Use only a short portion of the GUID to avoid MAX_PATH issues.
370 + GUID sessionGuid;
371 + VERIFY_SUCCEEDED(CoCreateGuid(&sessionGuid));
372 + auto guidStr = wsl::shared::string::GuidToString<wchar_t>(sessionGuid, wsl::shared::string::GuidToStringFlags::None);
373 + const auto sessionName = std::format(L"wslc-test-{}", guidStr.substr(0, 8));
374 +
375 + auto session = TestSession::Create(sessionName);
376 +
377 + // Load the Debian image into the test session to avoid hitting Docker Hub rate limits.
378 + EnsureImageIsLoaded(DebianTestImage(), session.Name());
379 +
380 + // Verify targeting a non-existent session fails.
381 + auto result = RunWslc(L"container list --session INVALID_SESSION_NAME");
382 + result.Verify({.Stdout = L"", .Stderr = L"Element not found. \r\nError code: ERROR_NOT_FOUND\r\n", .ExitCode = 1});
383 +
384 + // Verify session list
385 + result = RunWslc(L"session list");
386 + result.Verify({.Stderr = L"", .ExitCode = 0});
387 +
388 + // Verify there is a session with the name of the test session in the session list output.
389 + VERIFY_IS_TRUE(result.Stdout.has_value());
390 + auto findResult = result.Stdout->find(session.Name());
391 + VERIFY_ARE_NOT_EQUAL(findResult, std::wstring::npos);
392 +
393 + // Run container list in the test session, which should succeed if the session is valid.
394 + result = RunWslc(std::format(L"container list --session {}", session.Name()));
395 + result.Verify({.Stderr = L"", .ExitCode = 0});
396 +
397 + // Add a container to the new session.
398 + result = RunWslc(
399 + std::format(L"container create --session {} --name {} {}", session.Name(), L"test-cont", DebianTestImage().NameAndTag()));
400 + result.Dump(); // Dump so it is easier to find any potential issues with the pull in the test output.
401 + result.Verify({.ExitCode = 0});
402 +
403 + // Verify container exists in the custom session
404 + VerifyContainerIsListed(L"test-cont", L"created", session.Name());
405 +
406 + // Verify container does not exist in the default CLI session.
407 + VerifyContainerIsNotListed(L"test-cont");
408 + }
409 +
410 + WSLC_TEST_METHOD(WSLCE2E_Session_Shell)
411 + {
412 + // Ensure sessions are created by running container list elevated and non-elevated.
413 + auto result = RunWslc(L"container list", ElevationType::NonElevated);
414 + result.Verify({.Stderr = L"", .ExitCode = 0});
415 + result = RunWslc(L"container list", ElevationType::Elevated);
416 + result.Verify({.Stderr = L"", .ExitCode = 0});
417 +
418 + {
419 + Log::Comment(L"Testing elevated interactive session");
420 + // Session shell should attach to the correct default session.
421 + // Test should be elevated, therefore this should be the admin session.
422 + auto session = RunWslcInteractive(L"session shell");
423 + VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
424 +
425 + session.ExpectStdout(VT::SESSION_PROMPT);
426 +
427 + session.WriteLine("echo hello");
428 + session.ExpectStdout(VT::RESET);
429 + session.ExpectCommandEcho("echo hello");
430 + session.ExpectStdout("hello\r\n");
431 + session.ExpectStdout(VT::SESSION_PROMPT);
432 +
433 + session.WriteLine("whoami");
434 + session.ExpectStdout(VT::RESET);
435 + session.ExpectCommandEcho("whoami");
436 + session.ExpectStdout("root\r\n");
437 + session.ExpectStdout(VT::SESSION_PROMPT);
438 +
439 + session.ExitAndVerifyNoErrors();
440 + auto exitCode = session.Wait();
441 + VERIFY_ARE_EQUAL(0, exitCode);
442 + }
443 + {
444 + Log::Comment(L"Testing non-elevated interactive session with explicit session name");
445 + // Non-Elevated session shell should attach to the wslc by name also.
446 + auto nonAdminName = GetExpectedDefaultSessionName(false);
447 + auto session = RunWslcInteractive(std::format(L"session shell {}", nonAdminName), ElevationType::NonElevated);
448 + VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
449 +
450 + session.ExpectStdout(VT::SESSION_PROMPT);
451 +
452 + session.WriteLine("echo hello");
453 + session.ExpectStdout(VT::RESET);
454 + session.ExpectCommandEcho("echo hello");
455 + session.ExpectStdout("hello\r\n");
456 + session.ExpectStdout(VT::SESSION_PROMPT);
457 +
458 + session.WriteLine("whoami");
459 + session.ExpectStdout(VT::RESET);
460 + session.ExpectCommandEcho("whoami");
461 + session.ExpectStdout("root\r\n");
462 + session.ExpectStdout(VT::SESSION_PROMPT);
463 +
464 + session.ExitAndVerifyNoErrors();
465 + auto exitCode = session.Wait();
466 + VERIFY_ARE_EQUAL(0, exitCode);
467 + }
468 + {
469 + Log::Comment(L"Testing elevated interactive session with explicit admin session name");
470 + // Elevated session shell should attach to the wslc by name also.
471 + auto adminName = GetExpectedDefaultSessionName(true);
472 + auto session = RunWslcInteractive(std::format(L"session shell {}", adminName), ElevationType::Elevated);
473 + VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
474 +
475 + session.ExpectStdout(VT::SESSION_PROMPT);
476 +
477 + session.WriteLine("echo hello");
478 + session.ExpectStdout(VT::RESET);
479 + session.ExpectCommandEcho("echo hello");
480 + session.ExpectStdout("hello\r\n");
481 + session.ExpectStdout(VT::SESSION_PROMPT);
482 +
483 + session.WriteLine("whoami");
484 + session.ExpectStdout(VT::RESET);
485 + session.ExpectCommandEcho("whoami");
486 + session.ExpectStdout("root\r\n");
487 + session.ExpectStdout(VT::SESSION_PROMPT);
488 +
489 + session.ExitAndVerifyNoErrors();
490 + auto exitCode = session.Wait();
491 + VERIFY_ARE_EQUAL(0, exitCode);
492 + }
493 + }
494 +
495 +private:
496 + std::wstring GetHelpMessage() const
497 + {
498 + std::wstringstream output;
499 + output << GetWslcHeader() //
500 + << GetDescription() //
501 + << GetUsage() //
502 + << GetAvailableCommands() //
503 + << GetAvailableOptions();
504 + return output.str();
505 + }
506 +
507 + std::wstring GetVersionMessage() const
508 + {
509 + return std::format(L"wslc {}\r\n", WSL_PACKAGE_VERSION);
510 + }
511 +
512 + std::wstring GetDescription() const
513 + {
514 + return L"WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL "
515 + L"containers from the command line.\r\n\r\n";
516 + }
517 +
518 + std::wstring GetUsage() const
519 + {
520 + return L"Usage: wslc [<command>] [<options>]\r\n\r\n";
521 + }
522 +
523 + std::wstring GetAvailableCommands() const
524 + {
525 + std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
526 + {L"container", Localization::WSLCCLI_ContainerCommandDesc()},
527 + {L"image", Localization::WSLCCLI_ImageCommandDesc()},
528 + {L"registry", Localization::WSLCCLI_RegistryCommandDesc()},
529 + {L"session", Localization::WSLCCLI_SessionCommandDesc()},
530 + {L"settings", Localization::WSLCCLI_SettingsCommandDesc()},
531 + {L"volume", Localization::WSLCCLI_VolumeCommandDesc()},
532 + {L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
533 + {L"build", Localization::WSLCCLI_ImageBuildDesc()},
534 + {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
535 + {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
536 + {L"images", Localization::WSLCCLI_ImageListDesc()},
537 + {L"inspect", Localization::WSLCCLI_InspectDesc()},
538 + {L"kill", Localization::WSLCCLI_ContainerKillDesc()},
539 + {L"list", Localization::WSLCCLI_ContainerListDesc()},
540 + {L"load", Localization::WSLCCLI_ImageLoadDesc()},
541 + {L"login", Localization::WSLCCLI_LoginDesc()},
542 + {L"logout", Localization::WSLCCLI_LogoutDesc()},
543 + {L"logs", Localization::WSLCCLI_ContainerLogsDesc()},
544 + {L"pull", Localization::WSLCCLI_ImagePullDesc()},
545 + {L"push", Localization::WSLCCLI_ImagePushDesc()},
546 + {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()},
547 + {L"rmi", Localization::WSLCCLI_ImageRemoveDesc()},
548 + {L"run", Localization::WSLCCLI_ContainerRunDesc()},
549 + {L"save", Localization::WSLCCLI_ImageSaveDesc()},
550 + {L"start", Localization::WSLCCLI_ContainerStartDesc()},
551 + {L"stop", Localization::WSLCCLI_ContainerStopDesc()},
552 + {L"tag", Localization::WSLCCLI_ImageTagDesc()},
553 + {L"version", Localization::WSLCCLI_VersionDesc()},
554 + };
555 +
556 + size_t maxLen = 0;
557 + for (const auto& [name, _] : entries)
558 + {
559 + maxLen = (std::max)(maxLen, name.size());
560 + }
561 +
562 + std::wstringstream commands;
563 + commands << Localization::WSLCCLI_AvailableCommands() << L"\r\n";
564 + for (const auto& [name, desc] : entries)
565 + {
566 + commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
567 + }
568 + commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n\r\n";
569 + return commands.str();
570 + }
571 +
572 + std::wstring GetAvailableOptions() const
573 + {
574 + std::wstringstream options;
575 + options << L"The following options are available:\r\n"
576 + << L" -v,--version Show version information for this tool\r\n"
577 + << L" -?,--help Shows help about the selected command\r\n"
578 + << L"\r\n";
579 + return options.str();
580 + }
581 +};
582 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EHelpers.cpp new
+495
@@ -0,0 +1,495 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EHelpers.cpp
8 +
9 +Abstract:
10 +
11 + This file contains helper functions for end-to-end tests of WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "WSLCSessionDefaults.h"
16 +#include "ImageModel.h"
17 +#include "VolumeModel.h"
18 +#include "windows/Common.h"
19 +#include "WSLCExecutor.h"
20 +#include "WSLCE2EHelpers.h"
21 +#include <JsonUtils.h>
22 +#include <wslutil.h>
23 +
24 +extern std::wstring g_testDataPath;
25 +
26 +namespace WSLCE2ETests {
27 +
28 +using namespace WEX::Logging;
29 +using namespace wsl::windows::common;
30 +
31 +namespace {
32 + // Lazily compute the session storage base path.
33 + struct SessionStorageBasePathAccessor
34 + {
35 + operator const std::filesystem::path&() const
36 + {
37 + static const std::filesystem::path basePath =
38 + std::filesystem::absolute(std::filesystem::current_path() / L"wslc-cli-test-sessions");
39 + return basePath;
40 + }
41 + };
42 +
43 + static wil::com_ptr<IWSLCSessionManager> OpenSessionManager()
44 + {
45 + wil::com_ptr<IWSLCSessionManager> sessionManager;
46 + VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
47 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
48 + return sessionManager;
49 + }
50 +
51 + wil::com_ptr<IWSLCSession> CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags Flags = WSLCSessionFlagsNone)
52 + {
53 + const auto sessionManager = OpenSessionManager();
54 + wil::com_ptr<IWSLCSession> session;
55 + VERIFY_SUCCEEDED(sessionManager->CreateSession(&sessionSettings, Flags, &session));
56 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
57 +
58 + WSLCSessionState state{};
59 + VERIFY_SUCCEEDED(session->GetState(&state));
60 + VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning);
61 +
62 + return session;
63 + }
64 +
65 + WSLCSessionSettings GetDefaultSessionSettings(LPCWSTR name, LPCWSTR storagePath, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone)
66 + {
67 + WSLCSessionSettings settings{};
68 + settings.DisplayName = name;
69 + settings.CpuCount = 4;
70 + settings.MemoryMb = 2048;
71 + settings.BootTimeoutMs = 30 * 1000;
72 + settings.StoragePath = storagePath;
73 + settings.MaximumStorageSizeMb = 4096; // 4GB.
74 + settings.NetworkingMode = networkingMode;
75 + return settings;
76 + }
77 +
78 + wil::com_ptr<IWSLCSession> CreateCustomSession(
79 + const std::wstring& sessionName, const std::filesystem::path& storagePath, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone)
80 + {
81 + WSLCSessionSettings sessionSettings = GetDefaultSessionSettings(sessionName.c_str(), storagePath.c_str(), networkingMode);
82 + return CreateSession(sessionSettings);
83 + }
84 +
85 + void CleanupCustomSession(wil::com_ptr<IWSLCSession>& session, const std::filesystem::path& storagePath)
86 + {
87 + if (session)
88 + {
89 + LOG_IF_FAILED(session->Terminate());
90 + }
91 +
92 + session.reset();
93 +
94 + if (!storagePath.empty())
95 + {
96 + std::error_code error;
97 + std::filesystem::remove_all(storagePath, error);
98 + if (error)
99 + {
100 + Log::Error(std::format(L"Failed to cleanup storage path {}: {}", storagePath.wstring(), error.message()).c_str());
101 + }
102 + }
103 + }
104 +} // namespace
105 +
106 +const TestImage& AlpineTestImage()
107 +{
108 + static const TestImage image{L"alpine", L"latest", std::filesystem::path{g_testDataPath} / L"alpine-latest.tar"};
109 + return image;
110 +}
111 +
112 +const TestImage& DebianTestImage()
113 +{
114 + static const TestImage image{L"debian", L"latest", std::filesystem::path{g_testDataPath} / L"debian-latest.tar"};
115 + return image;
116 +}
117 +
118 +const TestImage& PythonTestImage()
119 +{
120 + static const TestImage image{L"python", L"3.12-alpine", std::filesystem::path{g_testDataPath} / L"python-3_12-alpine.tar"};
121 + return image;
122 +}
123 +
124 +const TestImage& InvalidTestImage()
125 +{
126 + static const TestImage image{L"mcr.microsoft.com/invalid-image", L"latest", L"INVALID_PATH"};
127 + return image;
128 +}
129 +
130 +TestSession TestSession::Create(const std::wstring& displayName, WSLCNetworkingMode networkingMode)
131 +{
132 + const std::filesystem::path& basePath = SessionStorageBasePathAccessor();
133 + auto storagePath = basePath / displayName;
134 + auto session = CreateCustomSession(displayName, storagePath, networkingMode);
135 + return TestSession{displayName, storagePath.wstring(), std::move(session)};
136 +}
137 +
138 +TestSession::~TestSession()
139 +{
140 + CleanupCustomSession(m_session, m_storagePath);
141 +}
142 +
143 +void VerifyContainerIsListed(const std::wstring& containerNameOrId, const std::wstring& status, const std::wstring& sessionName)
144 +{
145 + std::wstring command = L"container list --no-trunc --all";
146 + if (!sessionName.empty())
147 + {
148 + command = std::format(L"container list --no-trunc --all --session {}", sessionName);
149 + }
150 +
151 + auto result = RunWslc(command);
152 + result.Verify({.Stderr = L"", .ExitCode = 0});
153 +
154 + auto outputLines = result.GetStdoutLines();
155 + for (const auto& line : outputLines)
156 + {
157 + if (line.find(containerNameOrId) != std::wstring::npos)
158 + {
159 + const std::wstring message = L"Container '" + containerNameOrId + L"' found in container list output but status '" +
160 + status + L"' was not found in the same line";
161 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(status), message.c_str());
162 + return;
163 + }
164 + }
165 +
166 + const std::wstring message = L"Container '" + containerNameOrId + L"' not found in container list output";
167 + VERIFY_FAIL(message.c_str());
168 +}
169 +
170 +void VerifyImageIsUsed(const TestImage& image)
171 +{
172 + auto result = RunWslc(L"container list --no-trunc --all");
173 + result.Verify({.Stderr = L"", .ExitCode = 0});
174 + auto outputLines = result.GetStdoutLines();
175 + for (const auto& line : outputLines)
176 + {
177 + if (line.find(image.NameAndTag()) != std::wstring::npos)
178 + {
179 + return;
180 + }
181 + }
182 +
183 + VERIFY_FAIL(std::format(L"Image '{}' not found in container list output", image.NameAndTag()).c_str());
184 +}
185 +
186 +void VerifyImageIsNotUsed(const TestImage& image)
187 +{
188 + auto result = RunWslc(L"container list --no-trunc --all");
189 + result.Verify({.Stderr = L"", .ExitCode = 0});
190 + auto outputLines = result.GetStdoutLines();
191 + for (const auto& line : outputLines)
192 + {
193 + if (line.find(image.NameAndTag()) != std::wstring::npos)
194 + {
195 + VERIFY_FAIL(std::format(L"Image '{}' found in container list output", image.NameAndTag()).c_str());
196 + }
197 + }
198 +}
199 +
200 +void VerifyImageIsListed(const TestImage& image)
201 +{
202 + auto result = RunWslc(L"image list --format json");
203 + result.Verify({.Stderr = L"", .ExitCode = 0});
204 + auto images = wsl::shared::FromJson<std::vector<wsl::windows::wslc::models::ImageInformation>>(result.Stdout.value().c_str());
205 + for (const auto& img : images)
206 + {
207 + if (img.Repository == wsl::shared::string::WideToMultiByte(image.Name) &&
208 + img.Tag == wsl::shared::string::WideToMultiByte(image.Tag))
209 + {
210 + return;
211 + }
212 + }
213 +
214 + VERIFY_FAIL(std::format(L"Image '{}' not found in image list output", image.NameAndTag()).c_str());
215 +}
216 +
217 +void VerifyVolumeIsListed(const std::wstring& volumeName)
218 +{
219 + auto result = RunWslc(L"volume list --format json");
220 + result.Verify({.Stderr = L"", .ExitCode = 0});
221 + auto volumes = wsl::shared::FromJson<std::vector<WSLCVolumeInformation>>(result.Stdout.value().c_str());
222 + for (const auto& vol : volumes)
223 + {
224 + if (vol.Name == wsl::shared::string::WideToMultiByte(volumeName))
225 + {
226 + return;
227 + }
228 + }
229 +
230 + VERIFY_FAIL(std::format(L"Volume '{}' not found in volume list output", volumeName).c_str());
231 +}
232 +
233 +void VerifyVolumeIsNotListed(const std::wstring& volumeName)
234 +{
235 + auto result = RunWslc(L"volume list --format json");
236 + result.Verify({.Stderr = L"", .ExitCode = 0});
237 + auto volumes = wsl::shared::FromJson<std::vector<WSLCVolumeInformation>>(result.Stdout.value().c_str());
238 + for (const auto& vol : volumes)
239 + {
240 + if (vol.Name == wsl::shared::string::WideToMultiByte(volumeName))
241 + {
242 + VERIFY_FAIL(std::format(L"Volume '{}' found in volume list output", volumeName).c_str());
243 + }
244 + }
245 +}
246 +
247 +std::string GetHashId(const std::string& id, bool fullId)
248 +{
249 + return wsl::windows::common::string::TruncateId(id, !fullId);
250 +}
251 +
252 +wslc_schema::InspectContainer InspectContainer(const std::wstring& containerName)
253 +{
254 + auto result = RunWslc(std::format(L"container inspect {}", containerName));
255 + result.Verify({.Stderr = L"", .ExitCode = 0});
256 + auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
257 + VERIFY_ARE_EQUAL(1u, inspectData.size());
258 + return inspectData[0];
259 +}
260 +
261 +wslc_schema::InspectImage InspectImage(const std::wstring& imageName)
262 +{
263 + auto result = RunWslc(std::format(L"image inspect {}", imageName));
264 + result.Verify({.Stderr = L"", .ExitCode = 0});
265 + auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::InspectImage>>(result.Stdout.value().c_str());
266 + VERIFY_ARE_EQUAL(1u, inspectData.size());
267 + return inspectData[0];
268 +}
269 +
270 +wslc_schema::InspectVolume InspectVolume(const std::wstring& volumeName)
271 +{
272 + auto result = RunWslc(std::format(L"volume inspect {}", volumeName));
273 + result.Verify({.Stderr = L"", .ExitCode = 0});
274 + auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::InspectVolume>>(result.Stdout.value().c_str());
275 + VERIFY_ARE_EQUAL(1u, inspectData.size());
276 + return inspectData[0];
277 +}
278 +
279 +void EnsureContainerDoesNotExist(const std::wstring& containerName)
280 +{
281 + auto listResult = RunWslc(L"container list --no-trunc --all");
282 + listResult.Verify({.Stderr = L"", .ExitCode = 0});
283 +
284 + auto stdoutLines = listResult.GetStdoutLines();
285 + for (const auto& line : stdoutLines)
286 + {
287 + if (line.find(containerName) != std::wstring::npos)
288 + {
289 + if (line.find(L"running") != std::wstring::npos)
290 + {
291 + auto result = RunWslc(std::format(L"container kill {}", containerName));
292 + // Tolerate WSLC_E_CONTAINER_NOT_FOUND - container already stopped/removed
293 + if (result.ExitCode != 0 &&
294 + (!result.Stderr.has_value() || result.Stderr.value().find(L"WSLC_E_CONTAINER_NOT_FOUND") == std::wstring::npos))
295 + {
296 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
297 + }
298 + }
299 +
300 + auto result = RunWslc(std::format(L"container remove --force {}", containerName));
301 + // Tolerate WSLC_E_CONTAINER_NOT_FOUND - container already removed
302 + if (result.ExitCode != 0 &&
303 + (!result.Stderr.has_value() || result.Stderr.value().find(L"WSLC_E_CONTAINER_NOT_FOUND") == std::wstring::npos))
304 + {
305 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
306 + }
307 + break;
308 + }
309 + }
310 +}
311 +
312 +std::vector<wsl::windows::wslc::models::ContainerInformation> ListAllContainers()
313 +{
314 + auto result = RunWslc(L"container list --all --format json");
315 + result.Verify({.Stderr = L"", .ExitCode = 0});
316 + return wsl::shared::FromJson<std::vector<wsl::windows::wslc::models::ContainerInformation>>(result.Stdout.value().c_str());
317 +}
318 +
319 +void EnsureImageContainersAreDeleted(const TestImage& image)
320 +{
321 + auto containers = ListAllContainers();
322 + for (const auto& container : containers)
323 + {
324 + auto nameAndTag = wsl::shared::string::WideToMultiByte(image.NameAndTag());
325 + if (container.Image.find(nameAndTag) != std::string::npos)
326 + {
327 + auto result = RunWslc(std::format(L"container remove --force {}", container.Id));
328 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
329 + }
330 + }
331 +}
332 +
333 +void EnsureImageIsDeleted(const TestImage& image)
334 +{
335 + auto result = RunWslc(L"image list -q");
336 + result.Verify({.Stderr = L"", .ExitCode = 0});
337 +
338 + auto outputLines = result.GetStdoutLines();
339 + for (const auto& line : outputLines)
340 + {
341 + if (line.find(image.NameAndTag()) != std::wstring::npos)
342 + {
343 + EnsureImageContainersAreDeleted(image);
344 + auto deleteResult = RunWslc(std::format(L"image delete --force {}", image.NameAndTag()));
345 + deleteResult.Verify({.Stderr = L"", .ExitCode = 0});
346 + break;
347 + }
348 + }
349 +}
350 +
351 +void EnsureImageIsLoaded(const TestImage& image, const std::wstring& sessionName)
352 +{
353 + std::wstring listCommand = L"image list -q";
354 + if (!sessionName.empty())
355 + {
356 + listCommand = std::format(L"image list -q --session \"{}\"", sessionName);
357 + }
358 +
359 + auto result = RunWslc(listCommand);
360 + result.Verify({.Stderr = L"", .ExitCode = 0});
361 +
362 + auto outputLines = result.GetStdoutLines();
363 + for (const auto& line : outputLines)
364 + {
365 + if (line.find(image.NameAndTag()) != std::wstring::npos)
366 + {
367 + return;
368 + }
369 + }
370 +
371 + // Image not found, load it
372 + std::wstring loadCommand = std::format(L"image load --input \"{}\"", image.Path.wstring());
373 + if (!sessionName.empty())
374 + {
375 + loadCommand = std::format(L"image load --input \"{}\" --session \"{}\"", image.Path.wstring(), sessionName);
376 + }
377 +
378 + auto loadResult = RunWslc(loadCommand);
379 + loadResult.Verify({.Stderr = L"", .ExitCode = 0});
380 +}
381 +
382 +void EnsureSessionIsTerminated(const std::wstring& sessionName)
383 +{
384 + std::wstring targetSession = sessionName;
385 + if (targetSession.empty())
386 + {
387 + auto isElevated = wsl::windows::common::security::IsTokenElevated(wil::open_current_access_token(TOKEN_QUERY).get());
388 + auto baseName = isElevated ? wsl::windows::wslc::DefaultAdminSessionName : wsl::windows::wslc::DefaultSessionName;
389 +
390 + wchar_t username[256 + 1] = {};
391 + DWORD usernameLen = ARRAYSIZE(username);
392 + THROW_IF_WIN32_BOOL_FALSE(GetUserNameW(username, &usernameLen));
393 +
394 + targetSession = std::format(L"{}-{}", baseName, username);
395 + }
396 +
397 + auto listResult = RunWslc(L"session list");
398 + listResult.Verify({.Stderr = L"", .ExitCode = 0});
399 +
400 + auto stdoutLines = listResult.GetStdoutLines();
401 + for (const auto& line : stdoutLines)
402 + {
403 + // Check if the line ends with the target session name
404 + if (line.size() >= targetSession.size() && line.compare(line.size() - targetSession.size(), targetSession.size(), targetSession) == 0)
405 + {
406 + auto result = RunWslc(std::format(L"session terminate \"{}\"", targetSession));
407 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
408 + break;
409 + }
410 + }
411 +}
412 +
413 +void EnsureVolumeDoesNotExist(const std::wstring& volumeName)
414 +{
415 + auto result = RunWslc(L"volume list --format json");
416 + result.Verify({.Stderr = L"", .ExitCode = 0});
417 + auto volumes = wsl::shared::FromJson<std::vector<WSLCVolumeInformation>>(result.Stdout.value().c_str());
418 + for (const auto& vol : volumes)
419 + {
420 + if (vol.Name == wsl::shared::string::WideToMultiByte(volumeName))
421 + {
422 + auto deleteResult = RunWslc(std::format(L"volume rm {}", volumeName));
423 + deleteResult.Verify({.Stderr = L"", .ExitCode = 0});
424 + break;
425 + }
426 + }
427 +}
428 +
429 +wil::com_ptr<IWSLCSession> OpenDefaultElevatedSession()
430 +{
431 + // Ensure the default elevated session exists before opening it via COM.
432 + RunWslcAndVerify(L"container list", {.Stderr = L"", .ExitCode = 0});
433 +
434 + wil::com_ptr<IWSLCSessionManager> sessionManager;
435 + VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
436 + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
437 +
438 + wil::com_ptr<IWSLCSession> session;
439 + VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(nullptr, &session));
440 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
441 +
442 + return std::move(session);
443 +}
444 +
445 +std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(IWSLCSession& session, const std::string& username, const std::string& password, USHORT port)
446 +{
447 + EnsureImageIsLoaded({L"wslc-registry", L"latest", GetTestImagePath("wslc-registry:latest")});
448 +
449 + std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
450 +
451 + if (!username.empty())
452 + {
453 + env.push_back(std::format("USERNAME={}", username));
454 + env.push_back(std::format("PASSWORD={}", password));
455 + }
456 +
457 + WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env);
458 + launcher.SetEntrypoint({"/entrypoint.sh"});
459 + launcher.AddPort(port, port, AF_INET);
460 +
461 + auto container = launcher.Launch(session, WSLCContainerStartFlagsNone);
462 +
463 + auto address = std::format("127.0.0.1:{}", port);
464 + auto url = std::format(L"http://{}/v2/", wsl::shared::string::MultiByteToWide(address));
465 +
466 + int expectedCode = username.empty() ? 200 : 401;
467 + ExpectHttpResponse(url.c_str(), expectedCode, true);
468 +
469 + return {std::move(container), std::move(address)};
470 +}
471 +
472 +std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress)
473 +{
474 + auto registryImage = std::format(L"{}/{}", registryAddress, imageName);
475 + RunWslcAndVerify(std::format(L"image tag {} {}", imageName, registryImage), {.ExitCode = 0});
476 + return registryImage;
477 +}
478 +
479 +void WriteTestFile(const std::filesystem::path& filePath, const std::vector<std::string>& lines)
480 +{
481 + std::ofstream file(filePath, std::ios::out | std::ios::trunc | std::ios::binary);
482 + VERIFY_IS_TRUE(file.is_open());
483 + for (const auto& line : lines)
484 + {
485 + file << line << "\n";
486 + }
487 +
488 + VERIFY_IS_TRUE(file.good());
489 +}
490 +
491 +std::wstring GetPythonHttpServerScript(uint16_t port)
492 +{
493 + return std::format(L"python3 -m http.server {}", port);
494 +}
495 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EHelpers.h new
+199
@@ -0,0 +1,199 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EHelpers.h
8 +
9 +Abstract:
10 +
11 + This file contains helper functions for WSLCE2E tests.
12 +--*/
13 +
14 +#pragma once
15 +
16 +#include "WSLCExecutor.h"
17 +#include <docker_schema.h>
18 +#include <chrono>
19 +#include <wslc_schema.h>
20 +#include <ContainerModel.h>
21 +#include <WSLCContainerLauncher.h>
22 +
23 +namespace WSLCE2ETests {
24 +
25 +// VT100/ANSI escape sequence constants for TTY testing
26 +namespace VT {
27 +// Bracketed paste mode control sequences
28 +#define VT_B_START "\x1b[?2004h" // Enable bracketed paste mode
29 +#define VT_B_END "\x1b[?2004l" // Disable bracketed paste mode
30 +
31 +// Color/formatting sequences
32 +#define VT_RESET "\x1b[0m" // Reset all attributes
33 +#define VT_RED "\x1b[1;31m" // Bold red text
34 +
35 +// Terminal control sequences
36 +#define VT_ERASE_LINE "\x1b[K" // Erase from cursor to end of line
37 +#define VT_CR "\r" // Carriage return
38 +
39 + // Prompt patterns used in WSLC.
40 + constexpr auto SESSION_PROMPT = VT_B_START VT_RED "root@ [ " VT_RESET "/" VT_RED " ]# ";
41 +
42 + // Constexpr representations of the control sequences for use in tests.
43 + constexpr auto B_START = VT_B_START;
44 + constexpr auto B_END = VT_B_END;
45 + constexpr auto RESET = VT_RESET;
46 + constexpr auto RED = VT_RED;
47 + constexpr auto ERASE_LINE = VT_ERASE_LINE;
48 + constexpr auto CR = VT_CR;
49 +
50 +// Remove macros to avoid polluting global namespace.
51 +#undef VT_B_START
52 +#undef VT_B_END
53 +#undef VT_RESET
54 +#undef VT_RED
55 +#undef VT_ERASE_LINE
56 +#undef VT_CR
57 +
58 + // Helper function to build container prompt
59 + inline std::string BuildContainerPrompt(const std::string& prompt, bool withBracketedPaste = true)
60 + {
61 + if (withBracketedPaste)
62 + {
63 + return std::format("{}{}", B_START, prompt);
64 + }
65 + return std::format("{}", prompt);
66 + }
67 +
68 + inline std::string BuildContainerAttachPrompt(const std::string& prompt)
69 + {
70 + return std::format("{}{}{}{}", CR, ERASE_LINE, CR, prompt);
71 + }
72 +} // namespace VT
73 +
74 +struct TestImage
75 +{
76 + std::wstring Name;
77 + std::wstring Tag;
78 + std::filesystem::path Path;
79 + std::wstring NameAndTag() const
80 + {
81 + return std::format(L"{}:{}", Name, Tag);
82 + }
83 +};
84 +
85 +const TestImage& AlpineTestImage();
86 +const TestImage& DebianTestImage();
87 +const TestImage& PythonTestImage();
88 +const TestImage& InvalidTestImage();
89 +
90 +struct TestSession
91 +{
92 + static TestSession Create(const std::wstring& displayName, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone);
93 +
94 + TestSession(std::wstring name, std::filesystem::path storagePath, wil::com_ptr<IWSLCSession> session) :
95 + m_name(std::move(name)), m_storagePath(std::move(storagePath)), m_session(std::move(session))
96 + {
97 + }
98 +
99 + ~TestSession();
100 +
101 + NON_COPYABLE(TestSession);
102 + NON_MOVABLE(TestSession);
103 +
104 + const std::wstring& Name() const
105 + {
106 + return m_name;
107 + }
108 +
109 + const std::filesystem::path& StoragePath() const
110 + {
111 + return m_storagePath;
112 + }
113 +
114 +private:
115 + std::wstring m_name;
116 + std::filesystem::path m_storagePath;
117 + wil::com_ptr<IWSLCSession> m_session;
118 +};
119 +
120 +void VerifyContainerIsListed(const std::wstring& containerName, const std::wstring& status, const std::wstring& sessionName = L"");
121 +void VerifyImageIsUsed(const TestImage& image);
122 +void VerifyImageIsNotUsed(const TestImage& image);
123 +void VerifyImageIsListed(const TestImage& image);
124 +void VerifyVolumeIsListed(const std::wstring& volumeName);
125 +void VerifyVolumeIsNotListed(const std::wstring& volumeName);
126 +
127 +std::string GetHashId(const std::string& id, bool fullId = false);
128 +wsl::windows::common::wslc_schema::InspectContainer InspectContainer(const std::wstring& containerName);
129 +wsl::windows::common::wslc_schema::InspectImage InspectImage(const std::wstring& imageName);
130 +wsl::windows::common::wslc_schema::InspectVolume InspectVolume(const std::wstring& volumeName);
131 +std::vector<wsl::windows::wslc::models::ContainerInformation> ListAllContainers();
132 +
133 +void EnsureContainerDoesNotExist(const std::wstring& containerName);
134 +void EnsureImageIsLoaded(const TestImage& image, const std::wstring& sessionName = L"");
135 +void EnsureImageIsDeleted(const TestImage& image);
136 +void EnsureImageContainersAreDeleted(const TestImage& image);
137 +void EnsureSessionIsTerminated(const std::wstring& sessionName = L"");
138 +void EnsureVolumeDoesNotExist(const std::wstring& volumeName);
139 +
140 +void WriteTestFile(const std::filesystem::path& filePath, const std::vector<std::string>& envVariableLines);
141 +std::wstring GetPythonHttpServerScript(uint16_t port);
142 +
143 +// Default timeout of 0 will execute once.
144 +template <typename IntervalRep, typename IntervalPeriod, typename TimeoutRep, typename TimeoutPeriod>
145 +void VerifyContainerIsNotListed(
146 + const std::wstring& containerNameOrId,
147 + std::chrono::duration<IntervalRep, IntervalPeriod> retryInterval,
148 + std::chrono::duration<TimeoutRep, TimeoutPeriod> timeout)
149 +{
150 + try
151 + {
152 + wsl::shared::retry::RetryWithTimeout<void>(
153 + [&containerNameOrId]() {
154 + auto result = RunWslc(L"container list --no-trunc --all");
155 + result.Verify({.Stderr = L"", .ExitCode = 0});
156 +
157 + auto outputLines = result.GetStdoutLines();
158 + for (const auto& line : outputLines)
159 + {
160 + if (line.find(containerNameOrId) != std::wstring::npos)
161 + {
162 + THROW_HR(E_FAIL);
163 + }
164 + }
165 + },
166 + retryInterval,
167 + timeout);
168 + }
169 + catch (...)
170 + {
171 + HRESULT hr = wil::ResultFromCaughtException();
172 + const bool hasTimeout = std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count() > 0;
173 + const std::wstring message =
174 + hr == E_FAIL
175 + ? std::format(L"Container '{}' found in container list output{}", containerNameOrId, hasTimeout ? L" after timeout" : L" but it should not be listed")
176 + : std::format(
177 + L"Unexpected error while verifying container '{}' is not listed: 0x{:08X}",
178 + containerNameOrId,
179 + static_cast<unsigned int>(hr));
180 + VERIFY_FAIL(message.c_str());
181 + }
182 +}
183 +
184 +inline void VerifyContainerIsNotListed(const std::wstring& containerNameOrId)
185 +{
186 + VerifyContainerIsNotListed(containerNameOrId, std::chrono::milliseconds(0), std::chrono::milliseconds(0));
187 +}
188 +
189 +wil::com_ptr<IWSLCSession> OpenDefaultElevatedSession();
190 +
191 +// Starts a local registry container with host networking using the COM API.
192 +// Returns the running container (holds it alive) and the registry address (e.g. "127.0.0.1:PORT").
193 +std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalRegistry(
194 + IWSLCSession& session, const std::string& username = "", const std::string& password = "", USHORT port = 5000);
195 +
196 +// Tags an image for a registry and returns the full registry image reference (e.g. "127.0.0.1:PORT/debian:latest").
197 +std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress);
198 +
199 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp new
+304
@@ -0,0 +1,304 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EImageBuildTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC image build.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +#include <fstream>
19 +
20 +namespace WSLCE2ETests {
21 +
22 +class WSLCE2EImageBuildTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EImageBuildTests)
25 +
26 + TEST_CLASS_SETUP(ClassSetup)
27 + {
28 + DeleteAllBuiltImages();
29 + EnsureImageIsLoaded(DebianTestImage());
30 + return true;
31 + }
32 +
33 + TEST_CLASS_CLEANUP(ClassCleanup)
34 + {
35 + DeleteAllBuiltImages();
36 + EnsureImageIsDeleted(DebianTestImage());
37 + return true;
38 + }
39 +
40 + TEST_METHOD_SETUP(MethodSetup)
41 + {
42 + DeleteAllBuiltImages();
43 + return true;
44 + }
45 +
46 + TEST_METHOD_CLEANUP(MethodCleanup)
47 + {
48 + DeleteAllBuiltImages();
49 + return true;
50 + }
51 +
52 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_EmptyContextDirectory_Success)
53 + {
54 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-empty-context";
55 + auto cleanup = SetupTestDirectory(testRoot);
56 +
57 + auto contextDir = testRoot / L"context";
58 + std::error_code ec;
59 + std::filesystem::create_directories(contextDir, ec);
60 + THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
61 +
62 + auto dockerfilePath = testRoot / L"Dockerfile";
63 + WriteTestFile(dockerfilePath, "FROM debian:latest\nCMD [\"echo\", \"wslc-e2e-build-ok\"]\n");
64 +
65 + auto buildResult = RunWslc(
66 + std::format(L"build \"{}\" -f \"{}\" -t {}", contextDir.wstring(), dockerfilePath.wstring(), BuiltImage.NameAndTag()));
67 + buildResult.Verify({.Stderr = L"", .ExitCode = 0});
68 +
69 + auto inspectData = InspectImage(BuiltImage.NameAndTag());
70 + VERIFY_IS_TRUE(inspectData.RepoTags.has_value());
71 + VERIFY_ARE_EQUAL(1u, inspectData.RepoTags.value().size());
72 + VERIFY_ARE_EQUAL(BuiltImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData.RepoTags.value()[0]));
73 + }
74 +
75 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_BuildArgsFileAndMultipleTags_Success)
76 + {
77 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-args-tags";
78 + auto cleanup = SetupTestDirectory(testRoot);
79 +
80 + auto contextDir = testRoot / L"context";
81 + std::error_code ec;
82 + std::filesystem::create_directories(contextDir, ec);
83 + THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
84 +
85 + // Create a simple file in the context directory
86 + auto filePath = contextDir / L"hello.txt";
87 + WriteTestFile(filePath, "hello from wslc build\n");
88 +
89 + auto dockerfilePath = testRoot / L"Dockerfile";
90 + WriteTestFile(
91 + dockerfilePath,
92 + "FROM debian:latest\n"
93 + "ARG TEST_LABEL=default_value\n"
94 + "LABEL test_label=$TEST_LABEL\n"
95 + "COPY hello.txt /hello.txt\n"
96 + "CMD [\"cat\", \"/hello.txt\"]\n");
97 +
98 + auto buildResult = RunWslc(std::format(
99 + L"build \"{}\" -f \"{}\" -t {} -t {} --build-arg TEST_LABEL=wslc_e2e_test",
100 + contextDir.wstring(),
101 + dockerfilePath.wstring(),
102 + BuiltImageTag1.NameAndTag(),
103 + BuiltImageTag2.NameAndTag()));
104 + buildResult.Verify({.Stderr = L"", .ExitCode = 0});
105 +
106 + // Verify both tags are present by inspecting each one
107 + auto inspectData1 = InspectImage(BuiltImageTag1.NameAndTag());
108 + VERIFY_IS_TRUE(inspectData1.RepoTags.has_value());
109 +
110 + auto inspectData2 = InspectImage(BuiltImageTag2.NameAndTag());
111 +
112 + // Both tags refer to the same image
113 + VERIFY_ARE_EQUAL(inspectData1.Id, inspectData2.Id);
114 +
115 + // Verify the build arg was applied as a label
116 + VERIFY_IS_TRUE(inspectData1.Config.has_value());
117 + VERIFY_IS_TRUE(inspectData1.Config.value().Labels.has_value());
118 + const auto& labels = inspectData1.Config.value().Labels.value();
119 + auto it = labels.find("test_label");
120 + VERIFY_IS_TRUE(it != labels.end());
121 + VERIFY_ARE_EQUAL(std::string("wslc_e2e_test"), it->second);
122 + }
123 +
124 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_Pull_Success)
125 + {
126 + SKIP_TEST_UNSTABLE(); // TODO: Enable when a private image source is available.
127 +
128 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-pull";
129 + auto cleanup = SetupTestDirectory(testRoot);
130 +
131 + auto contextDir = testRoot / L"context";
132 + std::error_code ec;
133 + std::filesystem::create_directories(contextDir, ec);
134 + THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
135 +
136 + auto dockerfilePath = testRoot / L"Dockerfile";
137 + WriteTestFile(dockerfilePath, "FROM debian:latest\nCMD [\"echo\", \"pull-ok\"]\n");
138 +
139 + // Build with --pull --verbose. When --pull causes docker to resolve the base image
140 + // from the registry, the FROM step includes a @sha256: digest (e.g.
141 + // "FROM docker.io/library/debian:latest@sha256:..."). Without --pull, no digest appears.
142 + auto buildResult = RunWslc(std::format(
143 + L"build \"{}\" -f \"{}\" -t {} --pull --verbose", contextDir.wstring(), dockerfilePath.wstring(), BuiltImagePull.NameAndTag()));
144 + buildResult.Verify({.Stderr = L"", .ExitCode = 0});
145 +
146 + VERIFY_IS_TRUE(buildResult.Stdout.has_value());
147 + VERIFY_IS_TRUE(buildResult.Stdout->find(L"@sha256:") != std::wstring::npos);
148 + }
149 +
150 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_Target_Success)
151 + {
152 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-target";
153 + auto cleanup = SetupTestDirectory(testRoot);
154 +
155 + auto contextDir = testRoot / L"context";
156 + std::error_code ec;
157 + std::filesystem::create_directories(contextDir, ec);
158 + THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
159 +
160 + auto dockerfilePath = testRoot / L"Dockerfile";
161 + WriteTestFile(
162 + dockerfilePath,
163 + "FROM debian:latest AS build-stage\n"
164 + "RUN echo build > /stage.txt\n"
165 + "\n"
166 + "FROM debian:latest AS final-stage\n"
167 + "COPY --from=build-stage /stage.txt /stage.txt\n"
168 + "CMD [\"cat\", \"/stage.txt\"]\n");
169 +
170 + auto buildResult = RunWslc(std::format(
171 + L"build \"{}\" -f \"{}\" -t {} --target build-stage", contextDir.wstring(), dockerfilePath.wstring(), BuiltImageTarget.NameAndTag()));
172 + buildResult.Verify({.Stderr = L"", .ExitCode = 0});
173 +
174 + auto inspectData = InspectImage(BuiltImageTarget.NameAndTag());
175 + VERIFY_IS_TRUE(inspectData.RepoTags.has_value());
176 + VERIFY_ARE_EQUAL(1u, inspectData.RepoTags.value().size());
177 + VERIFY_ARE_EQUAL(BuiltImageTarget.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData.RepoTags.value()[0]));
178 +
179 + // Verify that --target stopped at build-stage: the image should NOT have the CMD
180 + // from final-stage. If --target were ignored, the CMD would be ["cat", "/stage.txt"].
181 + VERIFY_IS_TRUE(inspectData.Config.has_value());
182 + const std::vector<std::string> finalStageCmd{"cat", "/stage.txt"};
183 + VERIFY_IS_TRUE(!inspectData.Config.value().Cmd.has_value() || inspectData.Config.value().Cmd.value() != finalStageCmd);
184 + }
185 +
186 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_DockerfileInContextDir_Success)
187 + {
188 + BuildFromContextFile(L"Dockerfile", BuiltImageDockerfile);
189 + }
190 +
191 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_ContainerfileInContextDir_Success)
192 + {
193 + BuildFromContextFile(L"Containerfile", BuiltImageContainerfile);
194 + }
195 +
196 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_BothDockerfileAndContainerfile_Fails)
197 + {
198 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-both-files";
199 + auto cleanup = SetupTestDirectory(testRoot);
200 +
201 + WriteTestFile(testRoot / L"Dockerfile", "FROM debian:latest\n");
202 + WriteTestFile(testRoot / L"Containerfile", "FROM debian:latest\n");
203 +
204 + auto buildResult = RunWslc(std::format(L"build \"{}\"", testRoot.wstring()));
205 + buildResult.Verify(
206 + {.Stderr =
207 + L"Both Dockerfile and Containerfile found. Use -f to select the file to use\r\nError code: E_INVALIDARG\r\n",
208 + .ExitCode = 1});
209 + }
210 +
211 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_NeitherDockerfileNorContainerfile_Fails)
212 + {
213 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-no-files";
214 + auto cleanup = SetupTestDirectory(testRoot);
215 +
216 + auto absolutePath = std::filesystem::absolute(testRoot);
217 + auto buildResult = RunWslc(std::format(L"build \"{}\"", testRoot.wstring()));
218 + buildResult.Verify(
219 + {.Stderr = std::format(L"No Containerfile or Dockerfile found in '{}'\r\nError code: E_INVALIDARG\r\n", absolutePath.wstring()),
220 + .ExitCode = 1});
221 + }
222 +
223 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_ContainerfileAccessDenied_Fails)
224 + {
225 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-access-denied";
226 + auto cleanup = SetupTestDirectory(testRoot);
227 +
228 + auto containerfilePath = testRoot / L"Containerfile";
229 + WriteTestFile(containerfilePath, "FROM debian:latest\n");
230 +
231 + // Deny read access so wslc cannot open the file.
232 + SetPathAccess(containerfilePath, GENERIC_READ, DENY_ACCESS);
233 +
234 + auto restore = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [containerfilePath]() { DeleteFileW(containerfilePath.c_str()); });
235 +
236 + auto absoluteContainerfilePath = std::filesystem::absolute(containerfilePath);
237 + auto buildResult = RunWslc(std::format(L"build \"{}\"", testRoot.wstring()));
238 + buildResult.Verify(
239 + {.Stderr = std::format(
240 + L"Failed to open '{}': Access is denied. \r\nError code: E_ACCESSDENIED\r\n", absoluteContainerfilePath.wstring()),
241 + .ExitCode = 1});
242 + }
243 +
244 +private:
245 + const TestImage BuiltImage{L"wslc-e2e-build-empty-context", L"latest", L""};
246 + const TestImage BuiltImageTag1{L"wslc-e2e-build-args-tags", L"v1", L""};
247 + const TestImage BuiltImageTag2{L"wslc-e2e-build-args-tags", L"v2", L""};
248 + const TestImage BuiltImagePull{L"wslc-e2e-build-pull", L"latest", L""};
249 + const TestImage BuiltImageTarget{L"wslc-e2e-build-target", L"latest", L""};
250 + const TestImage BuiltImageDockerfile{L"wslc-e2e-build-dockerfile-ctx", L"latest", L""};
251 + const TestImage BuiltImageContainerfile{L"wslc-e2e-build-containerfile-ctx", L"latest", L""};
252 +
253 + void BuildFromContextFile(const std::wstring& fileName, const TestImage& image)
254 + {
255 + auto testRoot = std::filesystem::current_path() / image.Name;
256 + auto cleanup = SetupTestDirectory(testRoot);
257 +
258 + WriteTestFile(testRoot / fileName, "FROM debian:latest\nCMD [\"echo\", \"build-ok\"]\n");
259 +
260 + auto buildResult = RunWslc(std::format(L"build \"{}\" -t {}", testRoot.wstring(), image.NameAndTag()));
261 + buildResult.Verify({.Stderr = L"", .ExitCode = 0});
262 +
263 + auto inspectData = InspectImage(image.NameAndTag());
264 + VERIFY_IS_TRUE(inspectData.RepoTags.has_value());
265 + VERIFY_ARE_EQUAL(1u, inspectData.RepoTags.value().size());
266 + VERIFY_ARE_EQUAL(image.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData.RepoTags.value()[0]));
267 + }
268 +
269 + void DeleteAllBuiltImages()
270 + {
271 + EnsureImageIsDeleted(BuiltImage);
272 + EnsureImageIsDeleted(BuiltImageTag1);
273 + EnsureImageIsDeleted(BuiltImageTag2);
274 + EnsureImageIsDeleted(BuiltImagePull);
275 + EnsureImageIsDeleted(BuiltImageTarget);
276 + EnsureImageIsDeleted(BuiltImageDockerfile);
277 + EnsureImageIsDeleted(BuiltImageContainerfile);
278 + }
279 +
280 + static auto SetupTestDirectory(const std::filesystem::path& testRoot)
281 + {
282 + std::error_code ec;
283 + std::filesystem::remove_all(testRoot, ec);
284 + THROW_HR_IF_MSG(E_FAIL, ec.value() != 0 && std::filesystem::exists(testRoot), "%hs", ec.message().c_str());
285 +
286 + std::filesystem::create_directories(testRoot, ec);
287 + THROW_HR_IF_MSG(E_FAIL, ec.value() != 0 || !std::filesystem::exists(testRoot), "%hs", ec.message().c_str());
288 +
289 + return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [testRoot]() {
290 + std::error_code removeError;
291 + std::filesystem::remove_all(testRoot, removeError);
292 + });
293 + }
294 +
295 + static void WriteTestFile(const std::filesystem::path& path, const std::string& content)
296 + {
297 + std::ofstream file(path);
298 + THROW_HR_IF(E_FAIL, !file.is_open());
299 + file << content;
300 + THROW_HR_IF(E_FAIL, !file.good());
301 + file.close();
302 + }
303 +};
304 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageDeleteTests.cpp new
+166
@@ -0,0 +1,166 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EImageDeleteTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21 +
22 +class WSLCE2EImageDeleteTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EImageDeleteTests)
25 +
26 + TEST_METHOD_SETUP(MethodSetup)
27 + {
28 + EnsureContainerDoesNotExist(WslcContainerName);
29 + EnsureImageIsDeleted(DebianImage);
30 + return true;
31 + }
32 +
33 + TEST_CLASS_CLEANUP(ClassCleanup)
34 + {
35 + EnsureContainerDoesNotExist(WslcContainerName);
36 + EnsureImageIsDeleted(DebianImage);
37 + return true;
38 + }
39 +
40 + WSLC_TEST_METHOD(WSLCE2E_Image_Delete_HelpCommand)
41 + {
42 + auto result = RunWslc(L"image delete --help");
43 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
44 + }
45 +
46 + WSLC_TEST_METHOD(WSLCE2E_Image_Delete_ImageNotFound)
47 + {
48 + auto result = RunWslc(std::format(L"image delete {}", InvalidImage.Name));
49 + auto errorMessage = std::format(L"No such image: {}\r\nError code: WSLC_E_IMAGE_NOT_FOUND\r\n", InvalidImage.NameAndTag());
50 + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
51 + }
52 +
53 + WSLC_TEST_METHOD(WSLCE2E_Image_Delete_MissingImageName)
54 + {
55 + auto result = RunWslc(L"image delete");
56 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
57 + }
58 +
59 + WSLC_TEST_METHOD(WSLCE2E_Image_Delete_UnusedImage_Success)
60 + {
61 + EnsureImageIsLoaded(DebianImage);
62 + VerifyImageIsNotUsed(DebianImage);
63 +
64 + auto result = RunWslc(std::format(L"image delete {}", DebianImage.Name));
65 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
66 + }
67 +
68 + WSLC_TEST_METHOD(WSLCE2E_Image_Delete_UsedImage_Failure)
69 + {
70 + EnsureImageIsLoaded(DebianImage);
71 + VerifyImageIsNotUsed(DebianImage);
72 +
73 + auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
74 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
75 +
76 + VerifyImageIsUsed(DebianImage);
77 +
78 + auto inspectContainer = InspectContainer(WslcContainerName);
79 + auto containerId = GetHashId(inspectContainer.Id);
80 + auto inspectImage = InspectImage(DebianImage.NameAndTag());
81 + auto imageId = GetHashId(inspectImage.Id);
82 +
83 + auto result = RunWslc(std::format(L"image delete {}", DebianImage.Name));
84 + auto errorMessage = std::format(
85 + L"conflict: unable to remove repository reference \"{}\" (must force) - container {} is using its referenced image "
86 + L"{}\r\nError code: ERROR_SHARING_VIOLATION\r\n",
87 + DebianImage.Name,
88 + containerId,
89 + imageId);
90 + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
91 + }
92 +
93 + WSLC_TEST_METHOD(WSLCE2E_Image_DeleteForce_UsedImage_Success)
94 + {
95 + EnsureImageIsLoaded(DebianImage);
96 + VerifyImageIsNotUsed(DebianImage);
97 +
98 + auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
99 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
100 +
101 + VerifyImageIsUsed(DebianImage);
102 +
103 + auto result = RunWslc(std::format(L"image delete --force {}", DebianImage.Name));
104 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
105 + }
106 +
107 + WSLC_TEST_METHOD(WSLCE2E_Image_DeleteNoPrune)
108 + {
109 + // TODO: Implement once 'image tag' is implemented
110 + SKIP_TEST_NOT_IMPL();
111 + }
112 +
113 +private:
114 + const std::wstring WslcContainerName = L"wslc-test-container";
115 + const TestImage& DebianImage = DebianTestImage();
116 + const TestImage& InvalidImage = InvalidTestImage();
117 +
118 + std::wstring GetHelpMessage() const
119 + {
120 + std::wstringstream output;
121 + output << GetWslcHeader() //
122 + << GetDescription() //
123 + << GetUsage() //
124 + << GetAvailableCommandAliases() //
125 + << GetAvailableCommands() //
126 + << GetAvailableOptions();
127 + return output.str();
128 + }
129 +
130 + std::wstring GetDescription() const
131 + {
132 + return Localization::WSLCCLI_ImageRemoveLongDesc() + L"\r\n\r\n";
133 + }
134 +
135 + std::wstring GetUsage() const
136 + {
137 + return L"Usage: wslc image remove [<options>] <image>\r\n\r\n";
138 + }
139 +
140 + std::wstring GetAvailableCommandAliases() const
141 + {
142 + return L"The following command aliases are available: delete rm\r\n\r\n";
143 + }
144 +
145 + std::wstring GetAvailableCommands() const
146 + {
147 + std::wstringstream commands;
148 + commands << L"The following arguments are available:\r\n" //
149 + << L" image Image name\r\n" //
150 + << L"\r\n";
151 + return commands.str();
152 + }
153 +
154 + std::wstring GetAvailableOptions() const
155 + {
156 + std::wstringstream options;
157 + options << L"The following options are available:\r\n" //
158 + << L" -f,--force Delete images even if they are being used\r\n" //
159 + << L" --no-prune Do not delete untagged parents\r\n" //
160 + << L" --session Specify the session to use\r\n" //
161 + << L" -?,--help Shows help about the selected command\r\n" //
162 + << L"\r\n";
163 + return options.str();
164 + }
165 +};
166 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EImageInspectTests.cpp new
+114
@@ -0,0 +1,114 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EImageInspectTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +#include <wslc_schema.h>
19 +
20 +namespace WSLCE2ETests {
21 +using namespace wsl::shared;
22 +
23 +class WSLCE2EImageInspectTests
24 +{
25 + WSLC_TEST_CLASS(WSLCE2EImageInspectTests)
26 +
27 + TEST_CLASS_SETUP(ClassSetup)
28 + {
29 + EnsureImageIsLoaded(DebianImage);
30 + return true;
31 + }
32 +
33 + TEST_CLASS_CLEANUP(ClassCleanup)
34 + {
35 + EnsureImageIsDeleted(DebianImage);
36 + return true;
37 + }
38 +
39 + WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_HelpCommand)
40 + {
41 + auto result = RunWslc(L"image inspect --help");
42 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
43 + }
44 +
45 + WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_MissingImageName)
46 + {
47 + auto result = RunWslc(L"image inspect");
48 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
49 + }
50 +
51 + WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_ImageNotFound)
52 + {
53 + auto result = RunWslc(std::format(L"image inspect {}", InvalidImage.NameAndTag()));
54 + result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Image '{}' not found.\r\n", InvalidImage.NameAndTag()), .ExitCode = 1});
55 + }
56 +
57 + WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_Success)
58 + {
59 + auto result = RunWslc(std::format(L"image inspect {}", DebianImage.NameAndTag()));
60 + result.Verify({.Stderr = L"", .ExitCode = 0});
61 + auto inspectData =
62 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
63 + VERIFY_ARE_EQUAL(1u, inspectData.size());
64 + VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
65 + VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
66 + VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
67 + }
68 +
69 +private:
70 + const std::wstring WslcContainerName = L"wslc-test-container";
71 + const TestImage& DebianImage = DebianTestImage();
72 + const TestImage& InvalidImage = InvalidTestImage();
73 +
74 + std::wstring GetHelpMessage() const
75 + {
76 + std::wstringstream output;
77 + output << GetWslcHeader() //
78 + << GetDescription() //
79 + << GetUsage() //
80 + << GetAvailableCommands() //
81 + << GetAvailableOptions();
82 + return output.str();
83 + }
84 +
85 + std::wstring GetDescription() const
86 + {
87 + return Localization::WSLCCLI_ImageInspectLongDesc() + L"\r\n\r\n";
88 + }
89 +
90 + std::wstring GetUsage() const
91 + {
92 + return L"Usage: wslc image inspect [<options>] <image>\r\n\r\n";
93 + }
94 +
95 + std::wstring GetAvailableCommands() const
96 + {
97 + std::wstringstream commands;
98 + commands << L"The following arguments are available:\r\n" //
99 + << L" image Image name\r\n" //
100 + << L"\r\n";
101 + return commands.str();
102 + }
103 +
104 + std::wstring GetAvailableOptions() const
105 + {
106 + std::wstringstream options;
107 + options << L"The following options are available:\r\n" //
108 + << L" --session Specify the session to use\r\n" //
109 + << L" -?,--help Shows help about the selected command\r\n" //
110 + << L"\r\n";
111 + return options.str();
112 + }
113 +};
114 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EImageListTests.cpp new
+175
@@ -0,0 +1,175 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EImageListTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "ImageModel.h"
17 +#include "WSLCExecutor.h"
18 +#include "WSLCE2EHelpers.h"
19 +
20 +namespace WSLCE2ETests {
21 +using namespace wsl::shared;
22 +
23 +using namespace wsl::windows::wslc::models;
24 +
25 +class WSLCE2EImageListTests
26 +{
27 + WSLC_TEST_CLASS(WSLCE2EImageListTests)
28 +
29 + TEST_CLASS_SETUP(ClassSetup)
30 + {
31 + EnsureImageIsLoaded(DebianImage);
32 + EnsureImageIsLoaded(AlpineImage);
33 + return true;
34 + }
35 +
36 + TEST_CLASS_CLEANUP(ClassCleanup)
37 + {
38 + EnsureImageIsDeleted(DebianImage);
39 + EnsureImageIsDeleted(AlpineImage);
40 + return true;
41 + }
42 +
43 + WSLC_TEST_METHOD(WSLCE2E_Image_List_HelpCommand)
44 + {
45 + const auto result = RunWslc(L"image list --help");
46 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
47 + }
48 +
49 + WSLC_TEST_METHOD(WSLCE2E_Image_List_DisplayLoadedImage)
50 + {
51 + const auto result = RunWslc(L"image list");
52 + result.Verify({.Stderr = L"", .ExitCode = 0});
53 + for (const auto& line : result.GetStdoutLines())
54 + {
55 + if (line.find(DebianImage.Name) != std::wstring::npos && line.find(DebianImage.Tag) != std::wstring::npos)
56 + {
57 + return;
58 + }
59 + }
60 +
61 + VERIFY_FAIL(L"Failed to find the loaded image in the output");
62 + }
63 +
64 + WSLC_TEST_METHOD(WSLCE2E_Image_List_QuietOption_OutputsNamesOnly)
65 + {
66 + const auto result = RunWslc(L"image list --quiet");
67 + result.Verify({.Stderr = L"", .ExitCode = 0});
68 +
69 + bool imageFound = false;
70 + for (const auto& line : result.GetStdoutLines())
71 + {
72 + if (line == DebianImage.NameAndTag())
73 + {
74 + imageFound = true;
75 + break;
76 + }
77 + }
78 +
79 + VERIFY_IS_TRUE(imageFound);
80 + }
81 +
82 + WSLC_TEST_METHOD(WSLCE2E_Image_List_InvalidFormatOption)
83 + {
84 + const auto result = RunWslc(L"image list --format invalid");
85 + result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1});
86 + }
87 +
88 + WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat)
89 + {
90 + const auto result = RunWslc(L"image list --format json");
91 + result.Verify({.Stderr = L"", .ExitCode = 0});
92 +
93 + const auto images = wsl::shared::FromJson<std::vector<ImageInformation>>(result.Stdout.value().c_str());
94 +
95 + VERIFY_ARE_EQUAL(2u, images.size());
96 +
97 + std::vector<std::wstring> imageNames;
98 + for (const auto& image : images)
99 + {
100 + auto nameAndTag = std::format(
101 + L"{}:{}",
102 + wsl::shared::string::MultiByteToWide(image.Repository.value_or("<untagged>")),
103 + wsl::shared::string::MultiByteToWide(image.Tag.value_or("<untagged>")));
104 + imageNames.push_back(nameAndTag);
105 + }
106 +
107 + VERIFY_ARE_NOT_EQUAL(imageNames.end(), std::find(imageNames.begin(), imageNames.end(), DebianImage.NameAndTag()));
108 + VERIFY_ARE_NOT_EQUAL(imageNames.end(), std::find(imageNames.begin(), imageNames.end(), AlpineImage.NameAndTag()));
109 + }
110 +
111 + WSLC_TEST_METHOD(WSLCE2E_Image_List_TableFormat_HasExpectedColumns)
112 + {
113 + const auto result = RunWslc(L"image list");
114 + result.Verify({.Stderr = L"", .ExitCode = 0});
115 +
116 + bool foundHeader = false;
117 + for (const auto& line : result.GetStdoutLines())
118 + {
119 + if (line.find(L"REPOSITORY") != std::wstring::npos && line.find(L"TAG") != std::wstring::npos &&
120 + line.find(L"IMAGE ID") != std::wstring::npos && line.find(L"CREATED") != std::wstring::npos &&
121 + line.find(L"SIZE") != std::wstring::npos)
122 + {
123 + foundHeader = true;
124 + break;
125 + }
126 + }
127 +
128 + VERIFY_IS_TRUE(foundHeader, L"Expected table header with REPOSITORY, TAG, IMAGE ID, CREATED, SIZE columns");
129 + }
130 +
131 +private:
132 + const TestImage& DebianImage = DebianTestImage();
133 + const TestImage& AlpineImage = AlpineTestImage();
134 +
135 + std::wstring GetHelpMessage() const
136 + {
137 + std::wstringstream output;
138 + output << GetWslcHeader() //
139 + << GetDescription() //
140 + << GetUsage() //
141 + << GetAvailableCommandAliases() //
142 + << GetAvailableOptions();
143 + return output.str();
144 + }
145 +
146 + std::wstring GetDescription() const
147 + {
148 + return Localization::WSLCCLI_ImageListLongDesc() + L"\r\n\r\n";
149 + }
150 +
151 + std::wstring GetUsage() const
152 + {
153 + return L"Usage: wslc image list [<options>]\r\n\r\n";
154 + }
155 +
156 + std::wstring GetAvailableCommandAliases() const
157 + {
158 + return L"The following command aliases are available: ls\r\n\r\n";
159 + }
160 +
161 + std::wstring GetAvailableOptions() const
162 + {
163 + std::wstringstream options;
164 + options << L"The following options are available:\r\n"
165 + << L" --format " << Localization::WSLCCLI_FormatArgDescription() << L"\r\n"
166 + << L" --no-trunc Do not truncate output\r\n"
167 + << L" -q,--quiet Outputs the container IDs only\r\n"
168 + << L" --session Specify the session to use\r\n"
169 + << L" --verbose Output verbose details\r\n"
170 + << L" -?,--help Shows help about the selected command\r\n"
171 + << L"\r\n";
172 + return options.str();
173 + }
174 +};
175 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp new
+161
@@ -0,0 +1,161 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EImagePruneTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC image prune command.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21 +
22 +class WSLCE2EImagePruneTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EImagePruneTests)
25 +
26 + TEST_CLASS_SETUP(ClassSetup)
27 + {
28 + EnsureImageIsLoaded(DebianImage);
29 + return true;
30 + }
31 +
32 + TEST_CLASS_CLEANUP(ClassCleanup)
33 + {
34 + EnsureImageIsLoaded(DebianImage);
35 + return true;
36 + }
37 +
38 + WSLC_TEST_METHOD(WSLCE2E_Image_Prune_HelpCommand)
39 + {
40 + const auto result = RunWslc(L"image prune --help");
41 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
42 + }
43 +
44 + WSLC_TEST_METHOD(WSLCE2E_Image_Prune_NoDanglingImages)
45 + {
46 + // Prune when no dangling images exist should succeed with zero reclaimed space
47 + const auto result = RunWslc(L"image prune");
48 + result.Verify({.Stderr = L"", .ExitCode = 0});
49 +
50 + VerifyStdoutContains(result, L"Total reclaimed space:");
51 + }
52 +
53 + WSLC_TEST_METHOD(WSLCE2E_Image_Prune_DanglingImage)
54 + {
55 + // Create a dangling image by overwriting its only tag with a different image.
56 + // 1. Tag debian as prune-target:v1
57 + // 2. Delete the original debian:latest tag so prune-target:v1 is the only reference
58 + // 3. Tag alpine as prune-target:v1, overwriting it — debian image is now dangling
59 + EnsureImageIsLoaded(AlpineImage);
60 + auto cleanup = wil::scope_exit([&]() {
61 + RunWslc(L"image prune");
62 + RunWslc(L"image delete prune-target:v1");
63 + EnsureImageIsDeleted(AlpineImage);
64 + EnsureImageIsLoaded(DebianImage);
65 + });
66 +
67 + RunWslc(std::format(L"image tag {} prune-target:v1", DebianImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0});
68 + RunWslc(std::format(L"image delete {}", DebianImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0});
69 + RunWslc(std::format(L"image tag {} prune-target:v1", AlpineImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0});
70 +
71 + // Now prune should remove the dangling (original debian) image
72 + const auto result = RunWslc(L"image prune");
73 + result.Verify({.Stderr = L"", .ExitCode = 0});
74 +
75 + bool foundDeleted = false;
76 + for (const auto& line : result.GetStdoutLines())
77 + {
78 + if (line.find(L"Deleted:") != std::wstring::npos || line.find(L"Untagged:") != std::wstring::npos)
79 + {
80 + foundDeleted = true;
81 + break;
82 + }
83 + }
84 +
85 + VERIFY_IS_TRUE(foundDeleted, L"Expected pruned image output");
86 + VerifyStdoutContains(result, L"Total reclaimed space:");
87 +
88 + // Verify alpine image is still present (prune should only remove dangling images)
89 + VerifyImageIsListed(AlpineImage);
90 + }
91 +
92 + WSLC_TEST_METHOD(WSLCE2E_Image_Prune_AllFlag)
93 + {
94 + auto cleanup = wil::scope_exit([&]() { EnsureImageIsLoaded(DebianImage); });
95 +
96 + // --all should prune unused images (not just dangling)
97 + const auto result = RunWslc(L"image prune --all");
98 + result.Verify({.Stderr = L"", .ExitCode = 0});
99 +
100 + VerifyStdoutContains(result, L"Total reclaimed space:");
101 +
102 + // Verify the image was actually pruned
103 + auto listResult = RunWslc(L"image list");
104 + listResult.Verify({.Stderr = L"", .ExitCode = 0});
105 + for (const auto& line : listResult.GetStdoutLines())
106 + {
107 + VERIFY_IS_FALSE(
108 + line.find(DebianImage.NameAndTag()) != std::wstring::npos,
109 + std::format(L"Image '{}' should have been pruned by --all", DebianImage.NameAndTag()).c_str());
110 + }
111 + }
112 +
113 +private:
114 + const TestImage& DebianImage = DebianTestImage();
115 + const TestImage& AlpineImage = AlpineTestImage();
116 +
117 + static void VerifyStdoutContains(const WSLCExecutionResult& result, const std::wstring& substring)
118 + {
119 + for (const auto& line : result.GetStdoutLines())
120 + {
121 + if (line.find(substring) != std::wstring::npos)
122 + {
123 + return;
124 + }
125 + }
126 +
127 + VERIFY_FAIL(std::format(L"Expected stdout to contain '{}'", substring).c_str());
128 + }
129 +
130 + std::wstring GetHelpMessage() const
131 + {
132 + std::wstringstream output;
133 + output << GetWslcHeader() //
134 + << GetDescription() //
135 + << GetUsage() //
136 + << GetAvailableOptions();
137 + return output.str();
138 + }
139 +
140 + std::wstring GetDescription() const
141 + {
142 + return Localization::WSLCCLI_ImagePruneLongDesc() + L"\r\n\r\n";
143 + }
144 +
145 + std::wstring GetUsage() const
146 + {
147 + return L"Usage: wslc image prune [<options>]\r\n\r\n";
148 + }
149 +
150 + std::wstring GetAvailableOptions() const
151 + {
152 + std::wstringstream options;
153 + options << L"The following options are available:\r\n"
154 + << L" -a,--all " << Localization::WSLCCLI_ImagePruneAllArgDescription() << L"\r\n"
155 + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
156 + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
157 + << L"\r\n";
158 + return options.str();
159 + }
160 +};
161 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp new
+179
@@ -0,0 +1,179 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EImageSaveTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC image save.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21 +
22 +class WSLCE2EImageSaveTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EImageSaveTests)
25 +
26 + TEST_CLASS_CLEANUP(ClassCleanup)
27 + {
28 + EnsureImageIsDeleted(DebianImage);
29 + return true;
30 + }
31 +
32 + TEST_METHOD_SETUP(MethodSetup)
33 + {
34 + EnsureImageIsLoaded(DebianImage);
35 + SavedArchivePath = wsl::windows::common::filesystem::GetTempFilename();
36 + return true;
37 + }
38 +
39 + TEST_METHOD_CLEANUP(MethodCleanup)
40 + {
41 + DeleteFileW(SavedArchivePath.c_str());
42 + return true;
43 + }
44 +
45 + WSLC_TEST_METHOD(WSLCE2E_Image_Save_HelpCommand)
46 + {
47 + auto result = RunWslc(L"image save --help");
48 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
49 + }
50 +
51 + WSLC_TEST_METHOD(WSLCE2E_Image_Save_MissingImageName)
52 + {
53 + const auto result = RunWslc(std::format(L"image save --output \"{}\"", SavedArchivePath.wstring()));
54 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
55 + }
56 +
57 + WSLC_TEST_METHOD(WSLCE2E_Image_Save_ImageNotFound)
58 + {
59 + const auto result = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), InvalidImage.NameAndTag()));
60 + result.Verify({.Stdout = L"", .Stderr = L"reference does not exist\r\nError code: E_FAIL\r\n", .ExitCode = 1});
61 + }
62 +
63 + WSLC_TEST_METHOD(WSLCE2E_Image_Save_Success)
64 + {
65 + const auto result = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag()));
66 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
67 +
68 + VERIFY_IS_TRUE(std::filesystem::exists(SavedArchivePath));
69 + auto sourceFileSize = std::filesystem::file_size(DebianImage.Path);
70 + auto archiveFileSize = std::filesystem::file_size(SavedArchivePath);
71 + VERIFY_ARE_EQUAL(sourceFileSize, archiveFileSize);
72 + }
73 +
74 + WSLC_TEST_METHOD(WSLCE2E_Image_Save_Load)
75 + {
76 + // Save source image
77 + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag()));
78 + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
79 +
80 + // Delete source image
81 + EnsureImageIsDeleted(DebianImage);
82 +
83 + // Load from saved archive
84 + auto loadResult = RunWslc(std::format(L"image load --input \"{}\"", SavedArchivePath.wstring()));
85 + loadResult.Verify({.Stderr = L"", .ExitCode = 0});
86 +
87 + // Run a container from the loaded image to verify it works
88 + auto runResult = RunWslc(std::format(L"container run --rm {} echo Hello from saved image!", DebianImage.NameAndTag()));
89 + runResult.Verify({.Stdout = L"Hello from saved image!\n", .Stderr = L"", .ExitCode = 0});
90 + }
91 +
92 + WSLC_TEST_METHOD(WSLCE2E_Image_Save_ToStdout_Success)
93 + {
94 + const auto result = RunWslcAndRedirectToFile(std::format(L"image save {}", DebianImage.NameAndTag()), SavedArchivePath);
95 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
96 +
97 + VERIFY_IS_TRUE(std::filesystem::exists(SavedArchivePath));
98 + auto sourceFileSize = std::filesystem::file_size(DebianImage.Path);
99 + auto archiveFileSize = std::filesystem::file_size(SavedArchivePath);
100 + VERIFY_ARE_EQUAL(sourceFileSize, archiveFileSize);
101 + }
102 +
103 + WSLC_TEST_METHOD(WSLCE2E_Image_Save_ToTerminal_Fail)
104 + {
105 + // TODO: Re-enable once the test is stable in console-less pipeline environments.
106 + // Opening CONOUT$ may fail when the process has no console attached.
107 + SKIP_TEST_UNSTABLE();
108 +
109 + const auto result = RunWslcAndRedirectToFile(std::format(L"image save {}", DebianImage.NameAndTag()));
110 + result.Verify(
111 + {.Stderr = L"Cannot write image to terminal. Use the -o flag or redirect stdout.\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
112 + }
113 +
114 + WSLC_TEST_METHOD(WSLCE2E_Image_Save_ToStdout_Load)
115 + {
116 + // Save source image
117 + auto saveResult = RunWslcAndRedirectToFile(std::format(L"image save {}", DebianImage.NameAndTag()), SavedArchivePath);
118 + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
119 +
120 + // Delete source image
121 + EnsureImageIsDeleted(DebianImage);
122 +
123 + // Load from saved archive
124 + auto loadResult = RunWslc(std::format(L"image load --input \"{}\"", SavedArchivePath.wstring()));
125 + loadResult.Verify({.Stderr = L"", .ExitCode = 0});
126 +
127 + // Run a container from the loaded image to verify it works
128 + auto runResult = RunWslc(std::format(L"container run --rm {} echo Hello from saved image!", DebianImage.NameAndTag()));
129 + runResult.Verify({.Stdout = L"Hello from saved image!\n", .Stderr = L"", .ExitCode = 0});
130 + }
131 +
132 +private:
133 + const TestImage DebianImage = DebianTestImage();
134 + const TestImage& InvalidImage = InvalidTestImage();
135 +
136 + std::filesystem::path SavedArchivePath{};
137 +
138 + std::wstring GetHelpMessage() const
139 + {
140 + std::wstringstream output;
141 + output << GetWslcHeader() //
142 + << GetDescription() //
143 + << GetUsage() //
144 + << GetAvailableCommands() //
145 + << GetAvailableOptions();
146 + return output.str();
147 + }
148 +
149 + std::wstring GetDescription() const
150 + {
151 + return Localization::WSLCCLI_ImageSaveLongDesc() + L"\r\n\r\n";
152 + }
153 +
154 + std::wstring GetUsage() const
155 + {
156 + return L"Usage: wslc image save [<options>] <image>\r\n\r\n";
157 + }
158 +
159 + std::wstring GetAvailableCommands() const
160 + {
161 + std::wstringstream commands;
162 + commands << L"The following arguments are available:\r\n" //
163 + << L" image Image name\r\n" //
164 + << L"\r\n";
165 + return commands.str();
166 + }
167 +
168 + std::wstring GetAvailableOptions() const
169 + {
170 + std::wstringstream options;
171 + options << L"The following options are available:\r\n" //
172 + << L" -o,--output Path for the saved image\r\n" //
173 + << L" --session Specify the session to use\r\n" //
174 + << L" -?,--help Shows help about the selected command\r\n" //
175 + << L"\r\n";
176 + return options.str();
177 + }
178 +};
179 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EImageTagTests.cpp new
+212
@@ -0,0 +1,212 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EImageTagTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +
21 +using namespace wsl::shared::string;
22 +
23 +class WSLCE2EImageTagTests
24 +{
25 + WSL_TEST_CLASS(WSLCE2EImageTagTests)
26 +
27 + TEST_METHOD_SETUP(MethodSetup)
28 + {
29 + EnsureImageIsDeleted(DebianTaggedImage);
30 + EnsureImageIsLoaded(DebianImage);
31 + EnsureImageIsLoaded(AlpineImage);
32 + return true;
33 + }
34 +
35 + TEST_CLASS_CLEANUP(ClassCleanup)
36 + {
37 + EnsureImageIsDeleted(DebianTaggedImage);
38 + EnsureImageIsDeleted(DebianImage);
39 + EnsureImageIsDeleted(AlpineImage);
40 + return true;
41 + }
42 +
43 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_HelpCommand)
44 + {
45 + auto result = RunWslc(L"image tag --help");
46 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
47 + }
48 +
49 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_MissingSourceAndTarget)
50 + {
51 + auto result = RunWslc(L"image tag");
52 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'source'\r\n", .ExitCode = 1});
53 + }
54 +
55 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_MissingTarget)
56 + {
57 + auto result = RunWslc(std::format(L"image tag {}", DebianImage.NameAndTag()));
58 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'target'\r\n", .ExitCode = 1});
59 + }
60 +
61 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_SourceImageNotFound)
62 + {
63 + auto result = RunWslc(std::format(L"image tag {} {}", InvalidImage.NameAndTag(), DebianTaggedImage.NameAndTag()));
64 + auto errorMessage = std::format(L"No such image: {}\r\nError code: WSLC_E_IMAGE_NOT_FOUND\r\n", InvalidImage.NameAndTag());
65 + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
66 + }
67 +
68 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_TargetImageWithDigest_Fail)
69 + {
70 + auto imageWithDigest = L"debian-mock:tag@sha256:11111111111111111111111111111111";
71 + auto result = RunWslc(std::format(L"image tag {} {}", DebianImage.NameAndTag(), imageWithDigest));
72 + auto errorMessage =
73 + std::format(L"Invalid image tag format: '{}'. Expected format is 'name:tag'\r\nError code: E_INVALIDARG\r\n", imageWithDigest);
74 + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
75 + }
76 +
77 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_Success)
78 + {
79 + auto result = RunWslc(std::format(L"image tag {} {}", DebianImage.NameAndTag(), DebianTaggedImage.NameAndTag()));
80 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
81 +
82 + VerifyImageIsListed(DebianImage);
83 + VerifyImageIsListed(DebianTaggedImage);
84 +
85 + auto resultSourceInspect = RunWslc(std::format(L"image inspect {}", DebianImage.NameAndTag()));
86 + resultSourceInspect.Verify({.Stderr = L"", .ExitCode = 0});
87 + auto sourceInspect = resultSourceInspect.Stdout;
88 +
89 + auto resultTargetInspect = RunWslc(std::format(L"image inspect {}", DebianTaggedImage.NameAndTag()));
90 + resultTargetInspect.Verify({.Stderr = L"", .ExitCode = 0});
91 + auto targetInspect = resultTargetInspect.Stdout;
92 +
93 + VERIFY_IS_TRUE(sourceInspect.has_value());
94 + VERIFY_IS_TRUE(targetInspect.has_value());
95 + VERIFY_ARE_EQUAL(sourceInspect, targetInspect);
96 + }
97 +
98 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_SourceAndTargetAreTheSame_Noop)
99 + {
100 + auto result = RunWslc(std::format(L"image tag {} {}", DebianImage.NameAndTag(), DebianImage.NameAndTag()));
101 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
102 +
103 + VerifyImageIsListed(DebianImage);
104 +
105 + auto imageInspect = InspectImage(DebianImage.NameAndTag());
106 + VERIFY_ARE_EQUAL(1u, imageInspect.RepoTags->size());
107 + VERIFY_ARE_EQUAL(imageInspect.RepoTags->at(0), WideToMultiByte(DebianImage.NameAndTag()));
108 + }
109 +
110 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_TargetAlreadyExists_OverwritesTarget)
111 + {
112 + {
113 + auto result = RunWslc(std::format(L"image tag {} {}", DebianImage.NameAndTag(), DebianTaggedImage.NameAndTag()));
114 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
115 +
116 + auto resultSourceInspect = RunWslc(std::format(L"image inspect {}", DebianImage.NameAndTag()));
117 + resultSourceInspect.Verify({.Stderr = L"", .ExitCode = 0});
118 + auto sourceInspect = resultSourceInspect.Stdout;
119 +
120 + auto resultTargetInspect = RunWslc(std::format(L"image inspect {}", DebianTaggedImage.NameAndTag()));
121 + resultTargetInspect.Verify({.Stderr = L"", .ExitCode = 0});
122 + auto targetInspect = resultTargetInspect.Stdout;
123 +
124 + VERIFY_IS_TRUE(sourceInspect.has_value());
125 + VERIFY_IS_TRUE(targetInspect.has_value());
126 + VERIFY_ARE_EQUAL(sourceInspect, targetInspect);
127 + }
128 +
129 + {
130 + auto result = RunWslc(std::format(L"image tag {} {}", AlpineImage.NameAndTag(), DebianTaggedImage.NameAndTag()));
131 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
132 +
133 + auto resultSourceInspect = RunWslc(std::format(L"image inspect {}", AlpineImage.NameAndTag()));
134 + resultSourceInspect.Verify({.Stderr = L"", .ExitCode = 0});
135 + auto sourceInspect = resultSourceInspect.Stdout;
136 +
137 + auto resultTargetInspect = RunWslc(std::format(L"image inspect {}", DebianTaggedImage.NameAndTag()));
138 + resultTargetInspect.Verify({.Stderr = L"", .ExitCode = 0});
139 + auto targetInspect = resultTargetInspect.Stdout;
140 +
141 + VERIFY_IS_TRUE(sourceInspect.has_value());
142 + VERIFY_IS_TRUE(targetInspect.has_value());
143 + VERIFY_ARE_EQUAL(sourceInspect, targetInspect);
144 + }
145 + }
146 +
147 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_DeleteSourceImage_TargetRemains)
148 + {
149 + auto result = RunWslc(std::format(L"image tag {} {}", DebianImage.NameAndTag(), DebianTaggedImage.NameAndTag()));
150 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
151 +
152 + EnsureImageIsDeleted(DebianImage);
153 + VerifyImageIsListed(DebianTaggedImage);
154 + }
155 +
156 + WSLC_TEST_METHOD(WSLCE2E_Image_Tag_DeleteTargetImage_SourceRemains)
157 + {
158 + auto result = RunWslc(std::format(L"image tag {} {}", DebianImage.NameAndTag(), DebianTaggedImage.NameAndTag()));
159 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
160 +
161 + EnsureImageIsDeleted(DebianTaggedImage);
162 + VerifyImageIsListed(DebianImage);
163 + }
164 +
165 +private:
166 + const TestImage& DebianImage = DebianTestImage();
167 + const TestImage& AlpineImage = AlpineTestImage();
168 + const TestImage& InvalidImage = InvalidTestImage();
169 + const TestImage DebianTaggedImage{L"debian", L"e2e-new-tag"};
170 +
171 + std::wstring GetHelpMessage() const
172 + {
173 + std::wstringstream output;
174 + output << GetWslcHeader() //
175 + << GetDescription() //
176 + << GetUsage() //
177 + << GetAvailableCommands() //
178 + << GetAvailableOptions();
179 + return output.str();
180 + }
181 +
182 + std::wstring GetDescription() const
183 + {
184 + return wsl::shared::Localization::WSLCCLI_ImageTagLongDesc() + L"\r\n\r\n";
185 + }
186 +
187 + std::wstring GetUsage() const
188 + {
189 + return L"Usage: wslc image tag [<options>] <source> <target>\r\n\r\n";
190 + }
191 +
192 + std::wstring GetAvailableCommands() const
193 + {
194 + std::wstringstream commands;
195 + commands << L"The following arguments are available:\r\n" //
196 + << L" source Current or existing image reference in the image-name[:tag] format\r\n" //
197 + << L" target New image reference in the image-name[:tag] format\r\n" //
198 + << L"\r\n";
199 + return commands.str();
200 + }
201 +
202 + std::wstring GetAvailableOptions() const
203 + {
204 + std::wstringstream options;
205 + options << L"The following options are available:\r\n" //
206 + << L" --session Specify the session to use\r\n" //
207 + << L" -?,--help Shows help about the selected command\r\n" //
208 + << L"\r\n";
209 + return options.str();
210 + }
211 +};
212 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EImageTests.cpp new
+107
@@ -0,0 +1,107 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EImageTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +#include "Argument.h"
19 +
20 +namespace WSLCE2ETests {
21 +using namespace wsl::shared;
22 +
23 +class WSLCE2EImageTests
24 +{
25 + WSLC_TEST_CLASS(WSLCE2EImageTests)
26 +
27 + WSLC_TEST_METHOD(WSLCE2E_Image_HelpCommand)
28 + {
29 + auto result = RunWslc(L"image --help");
30 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
31 + }
32 +
33 + WSLC_TEST_METHOD(WSLCE2E_Image_NoSubcommand_ShowsHelp)
34 + {
35 + auto result = RunWslc(L"image");
36 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
37 + }
38 +
39 + WSLC_TEST_METHOD(WSLCE2E_Image_InvalidCommand_DisplaysErrorMessage)
40 + {
41 + auto result = RunWslc(L"image INVALID_CMD");
42 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
43 + }
44 +
45 +private:
46 + std::wstring GetHelpMessage() const
47 + {
48 + std::wstringstream output;
49 + output << GetWslcHeader() //
50 + << GetDescription() //
51 + << GetUsage() //
52 + << GetAvailableCommands() //
53 + << GetAvailableOptions();
54 + return output.str();
55 + }
56 +
57 + std::wstring GetDescription() const
58 + {
59 + return Localization::WSLCCLI_ImageCommandLongDesc() + L"\r\n\r\n";
60 + }
61 +
62 + std::wstring GetUsage() const
63 + {
64 + return L"Usage: wslc image [<command>] [<options>]\r\n\r\n";
65 + }
66 +
67 + std::wstring GetAvailableCommands() const
68 + {
69 + std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
70 + {L"build", Localization::WSLCCLI_ImageBuildDesc()},
71 + {L"remove", Localization::WSLCCLI_ImageRemoveDesc()},
72 + {L"inspect", Localization::WSLCCLI_ImageInspectDesc()},
73 + {L"list", Localization::WSLCCLI_ImageListDesc()},
74 + {L"load", Localization::WSLCCLI_ImageLoadDesc()},
75 + {L"prune", Localization::WSLCCLI_ImagePruneDesc()},
76 + {L"pull", Localization::WSLCCLI_ImagePullDesc()},
77 + {L"push", Localization::WSLCCLI_ImagePushDesc()},
78 + {L"save", Localization::WSLCCLI_ImageSaveDesc()},
79 + {L"tag", Localization::WSLCCLI_ImageTagDesc()},
80 + };
81 +
82 + size_t maxLen = 0;
83 + for (const auto& [name, _] : entries)
84 + {
85 + maxLen = (std::max)(maxLen, name.size());
86 + }
87 +
88 + std::wstringstream commands;
89 + commands << Localization::WSLCCLI_AvailableSubcommands() << L"\r\n";
90 + for (const auto& [name, desc] : entries)
91 + {
92 + commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
93 + }
94 + commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n" << L"\r\n";
95 + return commands.str();
96 + }
97 +
98 + std::wstring GetAvailableOptions() const
99 + {
100 + std::wstringstream options;
101 + options << L"The following options are available:\r\n"
102 + << L" -?,--help Shows help about the selected command\r\n"
103 + << L"\r\n";
104 + return options.str();
105 + }
106 +};
107 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EInspectTests.cpp new
+305
@@ -0,0 +1,305 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EInspectTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +#include <wslc_schema.h>
19 +#include <JsonUtils.h>
20 +
21 +namespace WSLCE2ETests {
22 +using namespace wsl::shared;
23 +
24 +class WSLCE2EInspectTests
25 +{
26 + WSLC_TEST_CLASS(WSLCE2EInspectTests)
27 +
28 + TEST_CLASS_SETUP(ClassSetup)
29 + {
30 + EnsureImageIsLoaded(DebianImage);
31 + return true;
32 + }
33 +
34 + TEST_CLASS_CLEANUP(ClassCleanup)
35 + {
36 + EnsureContainerDoesNotExist(WslcContainerName);
37 + EnsureContainerDoesNotExist(DebianImage.Name);
38 + EnsureImageIsDeleted(DebianImage);
39 + return true;
40 + }
41 +
42 + TEST_METHOD_SETUP(MethodSetup)
43 + {
44 + EnsureContainerDoesNotExist(WslcContainerName);
45 + EnsureContainerDoesNotExist(DebianImage.Name);
46 + return true;
47 + }
48 +
49 + WSLC_TEST_METHOD(WSLCE2E_Inspect_HelpCommand)
50 + {
51 + auto result = RunWslc(L"inspect --help");
52 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
53 + }
54 +
55 + WSLC_TEST_METHOD(WSLCE2E_Inspect_MissingObjectId)
56 + {
57 + auto result = RunWslc(L"inspect");
58 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'object-id'\r\n", .ExitCode = 1});
59 + }
60 +
61 + WSLC_TEST_METHOD(WSLCE2E_Inspect_ObjectNotFound)
62 + {
63 + auto result = RunWslc(std::format(L"inspect {}", InvalidImage.NameAndTag()));
64 + result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Object not found: {}\r\n", InvalidImage.NameAndTag()), .ExitCode = 1});
65 + }
66 +
67 + WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_Success)
68 + {
69 + auto result = RunWslc(std::format(L"inspect {}", DebianImage.NameAndTag()));
70 + result.Verify({.Stderr = L"", .ExitCode = 0});
71 + auto inspectData =
72 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
73 + VERIFY_ARE_EQUAL(1u, inspectData.size());
74 + VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
75 + VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
76 + VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
77 + }
78 +
79 + WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_WithTypeFlag)
80 + {
81 + auto result = RunWslc(std::format(L"inspect --type image {}", DebianImage.NameAndTag()));
82 + result.Verify({.Stderr = L"", .ExitCode = 0});
83 + auto inspectData =
84 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
85 + VERIFY_ARE_EQUAL(1u, inspectData.size());
86 + VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
87 + VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
88 + }
89 +
90 + WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_TypeMismatch)
91 + {
92 + auto result = RunWslc(std::format(L"inspect --type container {}", DebianImage.NameAndTag()));
93 + result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Object not found: {}\r\n", DebianImage.NameAndTag()), .ExitCode = 1});
94 + }
95 +
96 + WSLC_TEST_METHOD(WSLCE2E_Inspect_Container_Success)
97 + {
98 + EnsureContainerDoesNotExist(WslcContainerName);
99 + auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
100 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
101 +
102 + auto result = RunWslc(std::format(L"inspect {}", WslcContainerName));
103 + result.Verify({.Stderr = L"", .ExitCode = 0});
104 + auto inspectData =
105 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
106 + VERIFY_ARE_EQUAL(1u, inspectData.size());
107 + VERIFY_ARE_EQUAL(WslcContainerName, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
108 + }
109 +
110 + WSLC_TEST_METHOD(WSLCE2E_Inspect_Volume_Success)
111 + {
112 + EnsureVolumeDoesNotExist(WslcVolumeName);
113 +
114 + auto createResult = RunWslc(std::format(L"volume create {}", WslcVolumeName));
115 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
116 + auto deleteVolume = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(WslcVolumeName); });
117 +
118 + auto result = RunWslc(std::format(L"inspect {}", WslcVolumeName));
119 + result.Verify({.Stderr = L"", .ExitCode = 0});
120 +
121 + auto inspectData =
122 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectVolume>>(result.Stdout.value().c_str());
123 + VERIFY_ARE_EQUAL(1u, inspectData.size());
124 + VERIFY_ARE_EQUAL(WslcVolumeName, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
125 + }
126 +
127 + WSLC_TEST_METHOD(WSLCE2E_Inspect_Container_PriorityOverImage)
128 + {
129 + // When a container and image share the same name and no --type is specified,
130 + // the container should be returned (container is checked first in InspectTasks).
131 + EnsureContainerDoesNotExist(DebianImage.Name);
132 + auto createResult = RunWslc(std::format(L"container create --name {} {}", DebianImage.Name, DebianImage.NameAndTag()));
133 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
134 +
135 + // No type specified
136 + {
137 + auto result = RunWslc(std::format(L"inspect {}", DebianImage.Name));
138 + result.Verify({.Stderr = L"", .ExitCode = 0});
139 + auto inspectData =
140 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
141 + VERIFY_ARE_EQUAL(1u, inspectData.size());
142 + VERIFY_ARE_EQUAL(DebianImage.Name, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
143 + }
144 +
145 + // With --type container
146 + {
147 + auto result = RunWslc(std::format(L"inspect --type container {}", DebianImage.Name));
148 + result.Verify({.Stderr = L"", .ExitCode = 0});
149 + auto inspectData =
150 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
151 + VERIFY_ARE_EQUAL(1u, inspectData.size());
152 + VERIFY_ARE_EQUAL(DebianImage.Name, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
153 + }
154 +
155 + // With --type image
156 + {
157 + auto result = RunWslc(std::format(L"inspect --type image {}", DebianImage.Name));
158 + result.Verify({.Stderr = L"", .ExitCode = 0});
159 + auto inspectData =
160 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
161 + VERIFY_ARE_EQUAL(1u, inspectData.size());
162 + VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
163 + VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
164 + VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
165 + }
166 + }
167 +
168 + WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_PriorityOverVolume)
169 + {
170 + // When an image and volume share the same name and no --type is specified,
171 + // the image should be returned (image is checked before volume in InspectTasks).
172 + EnsureVolumeDoesNotExist(DebianImage.Name);
173 + auto createResult = RunWslc(std::format(L"volume create {}", DebianImage.Name));
174 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
175 + auto deleteVolume = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(DebianImage.Name); });
176 +
177 + // No type specified
178 + {
179 + auto result = RunWslc(std::format(L"inspect {}", DebianImage.Name));
180 + result.Verify({.Stderr = L"", .ExitCode = 0});
181 +
182 + auto inspectData =
183 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
184 + VERIFY_ARE_EQUAL(1u, inspectData.size());
185 + VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
186 + VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
187 + VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
188 + }
189 +
190 + // With --type image
191 + {
192 + auto result = RunWslc(std::format(L"inspect --type image {}", DebianImage.Name));
193 + result.Verify({.Stderr = L"", .ExitCode = 0});
194 + auto inspectData =
195 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
196 + VERIFY_ARE_EQUAL(1u, inspectData.size());
197 + VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
198 + VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
199 + VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
200 + }
201 +
202 + // With --type volume
203 + {
204 + auto result = RunWslc(std::format(L"inspect --type volume {}", DebianImage.Name));
205 + result.Verify({.Stderr = L"", .ExitCode = 0});
206 + auto inspectData =
207 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectVolume>>(result.Stdout.value().c_str());
208 + VERIFY_ARE_EQUAL(1u, inspectData.size());
209 + VERIFY_ARE_EQUAL(DebianImage.Name, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
210 + }
211 + }
212 +
213 + WSLC_TEST_METHOD(WSLCE2E_Inspect_MultipleObjects)
214 + {
215 + EnsureContainerDoesNotExist(WslcContainerName);
216 + auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
217 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
218 +
219 + // Inspect both a container and an image in a single call
220 + auto result = RunWslc(std::format(L"inspect {} {}", WslcContainerName, DebianImage.NameAndTag()));
221 + result.Verify({.Stderr = L"", .ExitCode = 0});
222 +
223 + // The result should be a JSON array with 2 entries
224 + auto array = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(result.Stdout.value()));
225 + VERIFY_ARE_EQUAL(2u, array.size());
226 + }
227 +
228 + WSLC_TEST_METHOD(WSLCE2E_Inspect_MultipleObjects_PartialFailure)
229 + {
230 + // Inspect a valid image and an invalid object
231 + auto result = RunWslc(std::format(L"inspect {} {}", DebianImage.NameAndTag(), InvalidImage.NameAndTag()));
232 + result.Verify({.Stderr = std::format(L"Object not found: {}\r\n", InvalidImage.NameAndTag()), .ExitCode = 1});
233 +
234 + // Stdout should still contain the valid result in a JSON array
235 + auto array = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(result.Stdout.value()));
236 + VERIFY_ARE_EQUAL(1u, array.size());
237 + }
238 +
239 + WSLC_TEST_METHOD(WSLCE2E_Inspect_InvalidTypeValue)
240 + {
241 + auto result = RunWslc(std::format(L"inspect --type invalid {}", DebianImage.NameAndTag()));
242 + result.Verify({.Stderr = L"Invalid type value: invalid is not a recognized inspect type. Supported inspect types are: image, container, volume.\r\n", .ExitCode = 1});
243 + }
244 +
245 + WSLC_TEST_METHOD(WSLCE2E_Inspect_SkipsInvalidFormatError)
246 + {
247 + // Image name cannot be upper case, but root inspect command should skip this error and continue with the inspect instead of failing
248 + auto result = RunWslc(L"inspect UPPER_CASE_INVALID_IMAGE");
249 + result.Verify({.Stdout = L"[]\r\n", .Stderr = L"Object not found: UPPER_CASE_INVALID_IMAGE\r\n", .ExitCode = 1});
250 + }
251 +
252 + WSLC_TEST_METHOD(WSLCE2E_Inspect_SkipsInvalidTypeSpecifiedArgumentError)
253 + {
254 + // Container name cannot contain a colon, but root inspect command should skip this error and continue with the inspect instead of failing
255 + auto result = RunWslc(std::format(L"inspect {}", InvalidImage.NameAndTag()));
256 + result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Object not found: {}\r\n", InvalidImage.NameAndTag()), .ExitCode = 1});
257 + }
258 +
259 +private:
260 + const std::wstring WslcContainerName = L"wslc-inspect-test-container";
261 + const TestImage& DebianImage = DebianTestImage();
262 + const TestImage& InvalidImage = InvalidTestImage();
263 + const std::wstring WslcVolumeName = L"wslc-inspect-test-volume";
264 + std::wstring GetHelpMessage() const
265 + {
266 + std::wstringstream output;
267 + output << GetWslcHeader() //
268 + << GetDescription() //
269 + << GetUsage() //
270 + << GetAvailableCommands() //
271 + << GetAvailableOptions();
272 + return output.str();
273 + }
274 +
275 + std::wstring GetDescription() const
276 + {
277 + return Localization::WSLCCLI_InspectLongDesc() + L"\r\n\r\n";
278 + }
279 +
280 + std::wstring GetUsage() const
281 + {
282 + return L"Usage: wslc inspect [<options>] <object-id>\r\n\r\n";
283 + }
284 +
285 + std::wstring GetAvailableCommands() const
286 + {
287 + std::wstringstream commands;
288 + commands << L"The following arguments are available:\r\n" //
289 + << L" object-id Name or Id of any object type\r\n" //
290 + << L"\r\n";
291 + return commands.str();
292 + }
293 +
294 + std::wstring GetAvailableOptions() const
295 + {
296 + std::wstringstream options;
297 + options << L"The following options are available:\r\n" //
298 + << L" -t,--type Type of the object to inspect\r\n" //
299 + << L" --session Specify the session to use\r\n" //
300 + << L" -?,--help Shows help about the selected command\r\n" //
301 + << L"\r\n";
302 + return options.str();
303 + }
304 +};
305 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp new
+181
@@ -0,0 +1,181 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EPushPullTests.cpp
8 +
9 +Abstract:
10 +
11 + End-to-end tests for wslc image push and pull against a local registry.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "WSLCExecutor.h"
18 +#include "WSLCE2EHelpers.h"
19 +#include "Argument.h"
20 +
21 +namespace WSLCE2ETests {
22 +using namespace wsl::shared;
23 +
24 +class WSLCE2EPushPullTests
25 +{
26 + WSLC_TEST_CLASS(WSLCE2EPushPullTests)
27 +
28 + WSLC_TEST_METHOD(WSLCE2E_Image_Push_HelpCommand)
29 + {
30 + auto result = RunWslc(L"image push --help");
31 + result.Verify({.Stdout = GetPushHelpMessage(), .Stderr = L"", .ExitCode = 0});
32 + }
33 +
34 + WSLC_TEST_METHOD(WSLCE2E_Image_Push_RootAlias)
35 + {
36 + auto result = RunWslc(L"push --help");
37 + result.Verify({.Stdout = GetPushRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0});
38 + }
39 +
40 + WSLC_TEST_METHOD(WSLCE2E_Image_Pull_HelpCommand)
41 + {
42 + auto result = RunWslc(L"image pull --help");
43 + result.Verify({.Stdout = GetPullHelpMessage(), .Stderr = L"", .ExitCode = 0});
44 + }
45 +
46 + WSLC_TEST_METHOD(WSLCE2E_Image_Pull_RootAlias)
47 + {
48 + auto result = RunWslc(L"pull --help");
49 + result.Verify({.Stdout = GetPullRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0});
50 + }
51 +
52 + WSLC_TEST_METHOD(WSLCE2E_Image_PushPull)
53 + {
54 + const auto& debianImage = DebianTestImage();
55 + EnsureImageIsLoaded(debianImage);
56 +
57 + // Start a local registry without auth.
58 + auto session = OpenDefaultElevatedSession();
59 +
60 + {
61 + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, "", "", 15003);
62 + auto registryAddressW = string::MultiByteToWide(registryAddress);
63 +
64 + // Tag the image for the local registry.
65 + auto registryImage = TagImageForRegistry(debianImage.NameAndTag(), registryAddressW);
66 +
67 + auto tagCleanup = wil::scope_exit([&]() { RunWslc(std::format(L"image delete --force {}", registryImage)); });
68 +
69 + // Push should succeed.
70 + auto result = RunWslc(std::format(L"push {}", registryImage));
71 + result.Verify({.ExitCode = 0});
72 +
73 + // Delete the local copy and pull it back.
74 + RunWslcAndVerify(std::format(L"image delete --force {}", registryImage), {.ExitCode = 0});
75 +
76 + result = RunWslc(std::format(L"pull {}", registryImage));
77 + result.Verify({.Stderr = L"", .ExitCode = 0});
78 +
79 + // Verify the image is now present.
80 + result = RunWslc(L"image list -q");
81 + result.Verify({.Stderr = L"", .ExitCode = 0});
82 + VERIFY_IS_TRUE(result.Stdout.has_value());
83 + VERIFY_IS_TRUE(result.Stdout->find(registryImage) != std::wstring::npos);
84 + }
85 + }
86 +
87 + WSLC_TEST_METHOD(WSLCE2E_Image_Push_NonExistentImage)
88 + {
89 + auto result = RunWslc(L"push does-not-exist:latest");
90 + auto errorMessage = L"An image does not exist locally with the tag: does-not-exist\r\nError code: E_FAIL\r\n";
91 + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
92 + }
93 +
94 + WSLC_TEST_METHOD(WSLCE2E_Image_Pull_NonExistentImage)
95 + {
96 + auto result = RunWslc(L"pull does-not-exist:latest");
97 + auto errorMessage =
98 + L"pull access denied for does-not-exist, repository does not exist or may require 'docker login': denied: requested "
99 + L"access to the resource is denied\r\nError code: WSLC_E_IMAGE_NOT_FOUND\r\n";
100 + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
101 + }
102 +
103 +private:
104 + std::wstring GetPushHelpMessage() const
105 + {
106 + std::wstringstream output;
107 + output << GetWslcHeader() << GetPushDescription() << GetPushUsage() << GetAvailableArguments() << GetAvailableOptions();
108 + return output.str();
109 + }
110 +
111 + std::wstring GetPushRootAliasHelpMessage() const
112 + {
113 + std::wstringstream output;
114 + output << GetWslcHeader() << GetPushDescription() << GetPushRootUsage() << GetAvailableArguments() << GetAvailableOptions();
115 + return output.str();
116 + }
117 +
118 + std::wstring GetPullHelpMessage() const
119 + {
120 + std::wstringstream output;
121 + output << GetWslcHeader() << GetPullDescription() << GetPullUsage() << GetAvailableArguments() << GetAvailableOptions();
122 + return output.str();
123 + }
124 +
125 + std::wstring GetPullRootAliasHelpMessage() const
126 + {
127 + std::wstringstream output;
128 + output << GetWslcHeader() << GetPullDescription() << GetPullRootUsage() << GetAvailableArguments() << GetAvailableOptions();
129 + return output.str();
130 + }
131 +
132 + std::wstring GetPushDescription() const
133 + {
134 + return Localization::WSLCCLI_ImagePushLongDesc() + L"\r\n\r\n";
135 + }
136 +
137 + std::wstring GetPullDescription() const
138 + {
139 + return Localization::WSLCCLI_ImagePullLongDesc() + L"\r\n\r\n";
140 + }
141 +
142 + std::wstring GetPushUsage() const
143 + {
144 + return L"Usage: wslc image push [<options>] <image>\r\n\r\n";
145 + }
146 +
147 + std::wstring GetPushRootUsage() const
148 + {
149 + return L"Usage: wslc push [<options>] <image>\r\n\r\n";
150 + }
151 +
152 + std::wstring GetPullUsage() const
153 + {
154 + return L"Usage: wslc image pull [<options>] <image>\r\n\r\n";
155 + }
156 +
157 + std::wstring GetPullRootUsage() const
158 + {
159 + return L"Usage: wslc pull [<options>] <image>\r\n\r\n";
160 + }
161 +
162 + std::wstring GetAvailableArguments() const
163 + {
164 + std::wstringstream args;
165 + args << Localization::WSLCCLI_AvailableArguments() << L"\r\n"
166 + << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n"
167 + << L"\r\n";
168 + return args.str();
169 + }
170 +
171 + std::wstring GetAvailableOptions() const
172 + {
173 + std::wstringstream options;
174 + options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
175 + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
176 + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
177 + << L"\r\n";
178 + return options.str();
179 + }
180 +};
181 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp new
+292
@@ -0,0 +1,292 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2ERegistryTests.cpp
8 +
9 +Abstract:
10 +
11 + End-to-end tests for wslc registry login/logout auth flows against a local registry.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "WSLCExecutor.h"
18 +#include "WSLCE2EHelpers.h"
19 +#include "Argument.h"
20 +#include <wslutil.h>
21 +
22 +namespace WSLCE2ETests {
23 +using namespace wsl::shared;
24 +using namespace WEX::Logging;
25 +
26 +namespace {
27 +
28 + constexpr auto c_username = "wslctest";
29 + constexpr auto c_password = "password";
30 +
31 + void VerifyAuthFailure(const WSLCExecutionResult& result)
32 + {
33 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
34 + VERIFY_IS_TRUE(result.Stderr.has_value());
35 + VERIFY_IS_TRUE(result.Stderr->find(L"no basic auth credentials") != std::wstring::npos);
36 + }
37 +
38 + void VerifyLogoutSucceeds(const std::wstring& registryAddress)
39 + {
40 + auto result = RunWslc(std::format(L"logout {}", registryAddress));
41 + result.Verify({.Stdout = Localization::WSLCCLI_LogoutSucceeded(registryAddress) + L"\r\n", .Stderr = L"", .ExitCode = 0});
42 + }
43 +} // namespace
44 +
45 +class WSLCE2ERegistryTests
46 +{
47 + WSLC_TEST_CLASS(WSLCE2ERegistryTests)
48 +
49 + WSLC_TEST_METHOD(WSLCE2E_Registry_LoginLogout_PushPull_AuthFlow)
50 + {
51 + const auto& debianImage = DebianTestImage();
52 + EnsureImageIsLoaded(debianImage);
53 +
54 + auto session = OpenDefaultElevatedSession();
55 +
56 + {
57 + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15001);
58 + auto registryAddressW = string::MultiByteToWide(registryAddress);
59 +
60 + auto registryImageName = TagImageForRegistry(debianImage.NameAndTag(), registryAddressW);
61 +
62 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
63 + RunWslc(std::format(L"image delete --force {}", registryImageName));
64 + RunWslc(std::format(L"logout {}", registryAddressW));
65 + });
66 +
67 + // Negative path before login: push and pull should fail.
68 + auto result = RunWslc(std::format(L"push {}", registryImageName));
69 + VerifyAuthFailure(result);
70 +
71 + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0});
72 +
73 + result = RunWslc(std::format(L"pull {}", registryImageName));
74 + VerifyAuthFailure(result);
75 +
76 + // Login and verify that saved credentials are used for push/pull.
77 + result = RunWslc(std::format(
78 + L"login -u {} -p {} {}", string::MultiByteToWide(c_username), string::MultiByteToWide(c_password), registryAddressW));
79 + result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0});
80 +
81 + registryImageName = TagImageForRegistry(L"debian:latest", registryAddressW);
82 + result = RunWslc(std::format(L"push {}", registryImageName));
83 + result.Verify({.ExitCode = 0});
84 +
85 + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0});
86 + result = RunWslc(std::format(L"pull {}", registryImageName));
87 + result.Verify({.Stderr = L"", .ExitCode = 0});
88 +
89 + // Logout and verify both pull and push fail again.
90 + VerifyLogoutSucceeds(registryAddressW);
91 +
92 + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0});
93 + result = RunWslc(std::format(L"pull {}", registryImageName));
94 + VerifyAuthFailure(result);
95 +
96 + registryImageName = TagImageForRegistry(L"debian:latest", registryAddressW);
97 + result = RunWslc(std::format(L"push {}", registryImageName));
98 + VerifyAuthFailure(result);
99 +
100 + // Negative path for logout command: second logout should fail.
101 + result = RunWslc(std::format(L"logout {}", registryAddressW));
102 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
103 + VERIFY_IS_TRUE(result.Stderr.has_value());
104 + VERIFY_IS_TRUE(result.Stderr->find(L"Not logged in to") != std::wstring::npos);
105 + }
106 + }
107 +
108 + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_HelpCommand)
109 + {
110 + auto result = RunWslc(L"registry login --help");
111 + result.Verify({.Stdout = GetLoginHelpMessage(), .Stderr = L"", .ExitCode = 0});
112 + }
113 +
114 + WSLC_TEST_METHOD(WSLCE2E_Registry_Logout_HelpCommand)
115 + {
116 + auto result = RunWslc(L"registry logout --help");
117 + result.Verify({.Stdout = GetLogoutHelpMessage(), .Stderr = L"", .ExitCode = 0});
118 + }
119 +
120 + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_PasswordAndStdinMutuallyExclusive)
121 + {
122 + auto result = RunWslc(L"login -u testuser -p testpass --password-stdin localhost:15099");
123 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
124 + VERIFY_IS_TRUE(result.Stderr.has_value());
125 + VERIFY_IS_TRUE(result.Stderr->find(L"--password and --password-stdin are mutually exclusive") != std::wstring::npos);
126 + }
127 +
128 + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_PasswordStdinRequiresUsername)
129 + {
130 + auto result = RunWslc(L"login --password-stdin localhost:15099");
131 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
132 + VERIFY_IS_TRUE(result.Stderr.has_value());
133 + VERIFY_IS_TRUE(result.Stderr->find(L"Must provide --username with --password-stdin") != std::wstring::npos);
134 + }
135 +
136 + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_InvalidCredentials)
137 + {
138 + auto session = OpenDefaultElevatedSession();
139 +
140 + {
141 + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15003);
142 + auto registryAddressW = string::MultiByteToWide(registryAddress);
143 +
144 + // Login with wrong password should fail.
145 + {
146 + auto result =
147 + RunWslc(std::format(L"login -u {} -p wrongpassword {}", string::MultiByteToWide(c_username), registryAddressW));
148 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
149 + VERIFY_IS_TRUE(result.Stderr.has_value());
150 + VERIFY_IS_TRUE(result.Stderr->find(L"401 Unauthorized") != std::wstring::npos);
151 + }
152 +
153 + // Login with wrong username should fail.
154 + {
155 + auto result = RunWslc(std::format(L"login -u wronguser -p {} {}", string::MultiByteToWide(c_password), registryAddressW));
156 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
157 + VERIFY_IS_TRUE(result.Stderr.has_value());
158 + VERIFY_IS_TRUE(result.Stderr->find(L"401 Unauthorized") != std::wstring::npos);
159 + }
160 +
161 + // Login with correct credentials should still succeed after failed attempts.
162 + {
163 + auto result = RunWslc(std::format(
164 + L"login -u {} -p {} {}", string::MultiByteToWide(c_username), string::MultiByteToWide(c_password), registryAddressW));
165 + result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0});
166 +
167 + VerifyLogoutSucceeds(registryAddressW);
168 + }
169 + }
170 + }
171 +
172 + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_CredentialInputMethods)
173 + {
174 + auto session = OpenDefaultElevatedSession();
175 +
176 + {
177 + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15002);
178 + auto registryAddressW = string::MultiByteToWide(registryAddress);
179 + auto usernameW = string::MultiByteToWide(c_username);
180 + auto passwordW = string::MultiByteToWide(c_password);
181 +
182 + // Login with -u and -p flags.
183 + {
184 + auto result = RunWslc(std::format(L"login -u {} -p {} {}", usernameW, passwordW, registryAddressW));
185 + result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0});
186 +
187 + VerifyLogoutSucceeds(registryAddressW);
188 + }
189 +
190 + // Login with -u and --password-stdin.
191 + {
192 + auto interactive = RunWslcInteractive(std::format(L"login -u {} --password-stdin {}", usernameW, registryAddressW));
193 + interactive.WriteLine(c_password);
194 + interactive.CloseStdin();
195 + auto exitCode = interactive.Wait();
196 + VERIFY_ARE_EQUAL(0, exitCode, L"Login with --password-stdin should succeed");
197 +
198 + VerifyLogoutSucceeds(registryAddressW);
199 + }
200 +
201 + // Login with interactive prompts (no flags).
202 + {
203 + auto interactive = RunWslcInteractive(std::format(L"login {}", registryAddressW));
204 + interactive.ExpectStderr("Username: ");
205 + interactive.WriteLine(c_username);
206 + interactive.ExpectStderr("Password: ");
207 + interactive.WriteLine(c_password);
208 + auto exitCode = interactive.Wait();
209 + VERIFY_ARE_EQUAL(0, exitCode, L"Interactive login should succeed");
210 +
211 + VerifyLogoutSucceeds(registryAddressW);
212 + }
213 + }
214 + }
215 +
216 +private:
217 + std::wstring GetLoginHelpMessage() const
218 + {
219 + std::wstringstream output;
220 + output << GetWslcHeader() << GetLoginDescription() << GetLoginUsage() << GetLoginAvailableArguments() << GetLoginAvailableOptions();
221 + return output.str();
222 + }
223 +
224 + std::wstring GetLogoutHelpMessage() const
225 + {
226 + std::wstringstream output;
227 + output << GetWslcHeader() << GetLogoutDescription() << GetLogoutUsage() << GetLogoutAvailableArguments()
228 + << GetLogoutAvailableOptions();
229 + return output.str();
230 + }
231 +
232 + std::wstring GetLoginDescription() const
233 + {
234 + return Localization::WSLCCLI_LoginLongDesc() + L"\r\n\r\n";
235 + }
236 +
237 + std::wstring GetLogoutDescription() const
238 + {
239 + return Localization::WSLCCLI_LogoutLongDesc() + L"\r\n\r\n";
240 + }
241 +
242 + std::wstring GetLoginUsage() const
243 + {
244 + return L"Usage: wslc registry login [<options>] [<server>]\r\n\r\n";
245 + }
246 +
247 + std::wstring GetLogoutUsage() const
248 + {
249 + return L"Usage: wslc registry logout [<options>] [<server>]\r\n\r\n";
250 + }
251 +
252 + std::wstring GetLoginAvailableArguments() const
253 + {
254 + std::wstringstream args;
255 + args << Localization::WSLCCLI_AvailableArguments() << L"\r\n"
256 + << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n"
257 + << L"\r\n";
258 + return args.str();
259 + }
260 +
261 + std::wstring GetLogoutAvailableArguments() const
262 + {
263 + std::wstringstream args;
264 + args << Localization::WSLCCLI_AvailableArguments() << L"\r\n"
265 + << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n"
266 + << L"\r\n";
267 + return args.str();
268 + }
269 +
270 + std::wstring GetLoginAvailableOptions() const
271 + {
272 + std::wstringstream options;
273 + options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
274 + << L" -p,--password " << Localization::WSLCCLI_LoginPasswordArgDescription() << L"\r\n"
275 + << L" --password-stdin " << Localization::WSLCCLI_LoginPasswordStdinArgDescription() << L"\r\n"
276 + << L" -u,--username " << Localization::WSLCCLI_LoginUsernameArgDescription() << L"\r\n"
277 + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
278 + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
279 + << L"\r\n";
280 + return options.str();
281 + }
282 +
283 + std::wstring GetLogoutAvailableOptions() const
284 + {
285 + std::wstringstream options;
286 + options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
287 + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
288 + << L"\r\n";
289 + return options.str();
290 + }
291 +};
292 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp new
+118
@@ -0,0 +1,118 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2ESessionEnterTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for the wslc session enter command.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCCLITestHelpers.h"
17 +#include "WSLCExecutor.h"
18 +#include "WSLCE2EHelpers.h"
19 +#include "WSLCSessionDefaults.h"
20 +#include "WSLCUserSettings.h"
21 +
22 +using namespace WEX::Logging;
23 +
24 +namespace WSLCE2ETests {
25 +
26 +namespace {
27 +
28 + const std::filesystem::path& GetDefaultStoragePath()
29 + {
30 + auto isElevated = wsl::windows::common::security::IsTokenElevated(wil::open_current_access_token(TOKEN_QUERY).get());
31 +
32 + static const std::filesystem::path basePath =
33 + wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / wsl::windows::wslc::DefaultStorageSubPath;
34 +
35 + // Session names are now qualified with the username (e.g. "wslc-cli-alice").
36 + wchar_t username[256 + 1] = {};
37 + DWORD usernameLen = ARRAYSIZE(username);
38 + THROW_IF_WIN32_BOOL_FALSE(GetUserNameW(username, &usernameLen));
39 +
40 + auto adminName = std::format(L"{}-{}", wsl::windows::wslc::DefaultAdminSessionName, username);
41 + auto nonAdminName = std::format(L"{}-{}", wsl::windows::wslc::DefaultSessionName, username);
42 +
43 + static const std::filesystem::path storagePathNonAdmin = basePath / nonAdminName;
44 + static const std::filesystem::path storagePathAdmin = basePath / adminName;
45 +
46 + return isElevated ? storagePathAdmin : storagePathNonAdmin;
47 + }
48 +
49 +} // namespace
50 +
51 +class WSLCE2ESessionEnterTests
52 +{
53 + WSLC_TEST_CLASS(WSLCE2ESessionEnterTests)
54 +
55 + TEST_CLASS_SETUP(TestClassSetup)
56 + {
57 + // Ensure that the wslc cli session storage is created.
58 + RunWslc(L"image ls").Verify({.ExitCode = 0});
59 +
60 + // Terminate the wslc session since we use its storage path in this test class.
61 + RunWslc(L"session terminate");
62 + return true;
63 + }
64 +
65 + WSLC_TEST_METHOD(WSLCE2E_SessionEnter_WithName)
66 + {
67 + constexpr auto sessionName = L"test-wslc-session-enter";
68 +
69 + // Run an interactive session enter with an explicit name.
70 + auto session = RunWslcInteractive(std::format(L"session enter \"{}\" --name {}", GetDefaultStoragePath(), sessionName));
71 + VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
72 +
73 + session.ExpectStdout(VT::SESSION_PROMPT);
74 +
75 + // Validate that the shell is running as root.
76 + session.WriteLine("whoami");
77 + session.ExpectStdout(VT::RESET);
78 + session.ExpectCommandEcho("whoami");
79 + session.ExpectStdout("root\r\n");
80 + session.ExpectStdout(VT::SESSION_PROMPT);
81 +
82 + // Verify the session appears in session list.
83 + auto listResult = RunWslc(L"session list");
84 + listResult.Verify({.Stderr = L"", .ExitCode = S_OK});
85 + VERIFY_IS_TRUE(listResult.Stdout.has_value());
86 + VERIFY_IS_TRUE(listResult.Stdout->find(sessionName) != std::wstring::npos);
87 +
88 + // Exit the shell.
89 + VERIFY_ARE_EQUAL(session.Exit(), 0);
90 +
91 + // Verify the session is no longer in the session list after exiting.
92 + listResult = RunWslc(L"session list");
93 + listResult.Verify({.Stderr = L"", .ExitCode = S_OK});
94 + VERIFY_IS_TRUE(listResult.Stdout.has_value());
95 + VERIFY_IS_FALSE(listResult.Stdout->find(sessionName) != std::wstring::npos);
96 + }
97 +
98 + WSLC_TEST_METHOD(WSLCE2E_SessionEnter_WithoutName_GeneratesGuid)
99 + {
100 + auto session = RunWslcInteractive(std::format(L"session enter \"{}\"", GetDefaultStoragePath()));
101 + VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
102 +
103 + session.ExpectStderr("Created session: ");
104 + session.ExpectStdout(VT::SESSION_PROMPT);
105 +
106 + VERIFY_ARE_EQUAL(session.Exit(), 0);
107 + }
108 +
109 + WSLC_TEST_METHOD(WSLCE2E_SessionEnter_StoragePathNotFound)
110 + {
111 + auto result = RunWslc(L"session enter does-not-exist");
112 + result.Verify({
113 + .Stderr = L"The system cannot find the path specified. \r\nError code: ERROR_PATH_NOT_FOUND\r\n",
114 + .ExitCode = 1,
115 + });
116 + }
117 +};
118 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeCreateTests.cpp new
+158
@@ -0,0 +1,158 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EVolumeCreateTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "VolumeModel.h"
17 +#include "WSLCExecutor.h"
18 +#include "WSLCE2EHelpers.h"
19 +
20 +namespace WSLCE2ETests {
21 +using namespace wsl::shared;
22 +using namespace wsl::windows::wslc::models;
23 +
24 +class WSLCE2EVolumeCreateTests
25 +{
26 + WSLC_TEST_CLASS(WSLCE2EVolumeCreateTests)
27 +
28 + TEST_METHOD_SETUP(MethodSetup)
29 + {
30 + EnsureVolumeDoesNotExist(TestVolumeName);
31 + return true;
32 + }
33 +
34 + TEST_CLASS_CLEANUP(ClassCleanup)
35 + {
36 + EnsureVolumeDoesNotExist(TestVolumeName);
37 + return true;
38 + }
39 +
40 + WSLC_TEST_METHOD(WSLCE2E_Volume_Create_HelpCommand)
41 + {
42 + auto result = RunWslc(L"volume create --help");
43 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
44 + }
45 +
46 + WSLC_TEST_METHOD(WSLCE2E_Volume_Create_EmptyName)
47 + {
48 + auto result = RunWslc(L"volume create");
49 + result.Verify({.Stderr = L"", .ExitCode = 0});
50 + auto volumeName = result.GetStdoutOneLine();
51 + VERIFY_IS_FALSE(volumeName.empty());
52 +
53 + auto deleteVolume = wil::scope_exit([&]() {
54 + auto deleteResult = RunWslc(std::format(L"volume rm {}", volumeName));
55 + deleteResult.Verify({.Stderr = L"", .ExitCode = 0});
56 + });
57 + VerifyVolumeIsListed(volumeName);
58 + }
59 +
60 + WSLC_TEST_METHOD(WSLCE2E_Volume_Create_DefaultDriverIsGuest)
61 + {
62 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
63 + result.Verify({.Stderr = L"", .ExitCode = 0});
64 + VERIFY_ARE_EQUAL(TestVolumeName, result.GetStdoutOneLine());
65 +
66 + VerifyVolumeIsListed(TestVolumeName);
67 + auto inspect = InspectVolume(TestVolumeName);
68 + VERIFY_ARE_EQUAL("guest", inspect.Driver);
69 + }
70 +
71 + WSLC_TEST_METHOD(WSLCE2E_Volume_Create_ExplicitDriver_Success)
72 + {
73 + auto result = RunWslc(std::format(L"volume create --driver vhd --opt SizeBytes={} {}", DefaultVolumeSizeBytes, TestVolumeName));
74 + result.Verify({.Stderr = L"", .ExitCode = 0});
75 + VERIFY_ARE_EQUAL(TestVolumeName, result.GetStdoutOneLine());
76 +
77 + VerifyVolumeIsListed(TestVolumeName);
78 + auto inspect = InspectVolume(TestVolumeName);
79 + VERIFY_ARE_EQUAL("vhd", inspect.Driver);
80 + }
81 +
82 + WSLC_TEST_METHOD(WSLCE2E_Volume_Create_Vhd_MissingOpts_Fail)
83 + {
84 + auto result = RunWslc(std::format(L"volume create --driver vhd {}", TestVolumeName));
85 + result.Verify({.Stderr = L"Missing required option: 'SizeBytes'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
86 +
87 + VerifyVolumeIsNotListed(TestVolumeName);
88 + }
89 +
90 + WSLC_TEST_METHOD(WSLCE2E_Volume_Create_InvalidDriver_Fail)
91 + {
92 + auto result =
93 + RunWslc(std::format(L"volume create --driver invalid_driver --opt SizeBytes={} {}", DefaultVolumeSizeBytes, TestVolumeName));
94 + result.Verify({.Stdout = L"", .Stderr = L"Unsupported volume type: 'invalid_driver'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
95 +
96 + VerifyVolumeIsNotListed(TestVolumeName);
97 + }
98 +
99 + WSLC_TEST_METHOD(WSLCE2E_Volume_Create_WithLabel_Success)
100 + {
101 + auto result = RunWslc(std::format(L"volume create --label A=1 --label B=2 {}", TestVolumeName));
102 + result.Verify({.Stderr = L"", .ExitCode = 0});
103 + VERIFY_ARE_EQUAL(TestVolumeName, result.GetStdoutOneLine());
104 +
105 + VerifyVolumeIsListed(TestVolumeName);
106 + auto inspect = InspectVolume(TestVolumeName);
107 + VERIFY_ARE_EQUAL("1", inspect.Labels["A"]);
108 + VERIFY_ARE_EQUAL("2", inspect.Labels["B"]);
109 + }
110 +
111 +private:
112 + const std::wstring TestVolumeName = L"wslc-e2e-volume-create";
113 + const int DefaultVolumeSizeBytes = 3 * 1024 * 1024;
114 +
115 + std::wstring GetHelpMessage() const
116 + {
117 + std::wstringstream output;
118 + output << GetWslcHeader() //
119 + << GetDescription() //
120 + << GetUsage() //
121 + << GetAvailableCommands() //
122 + << GetAvailableOptions();
123 + return output.str();
124 + }
125 +
126 + std::wstring GetDescription() const
127 + {
128 + return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_VolumeCreateLongDesc());
129 + }
130 +
131 + std::wstring GetUsage() const
132 + {
133 + return L"Usage: wslc volume create [<options>] [<volume-name>]\r\n\r\n";
134 + }
135 +
136 + std::wstring GetAvailableCommands() const
137 + {
138 + std::wstringstream commands;
139 + commands << L"The following arguments are available:\r\n" //
140 + << L" volume-name Volume name\r\n" //
141 + << L"\r\n";
142 + return commands.str();
143 + }
144 +
145 + std::wstring GetAvailableOptions() const
146 + {
147 + std::wstringstream options;
148 + options << L"The following options are available:\r\n" //
149 + << L" -d,--driver Specify volume driver name (default guest)\r\n" //
150 + << L" -o,--opt Set driver specific options\r\n" //
151 + << L" -l,--label Set metadata on an object\r\n" //
152 + << L" --session Specify the session to use\r\n" //
153 + << L" -?,--help Shows help about the selected command\r\n" //
154 + << L"\r\n";
155 + return options.str();
156 + }
157 +};
158 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeInspectTests.cpp new
+165
@@ -0,0 +1,165 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EVolumeInspectTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21 +using namespace wsl::shared::string;
22 +
23 +class WSLCE2EVolumeInspectTests
24 +{
25 + WSLC_TEST_CLASS(WSLCE2EVolumeInspectTests)
26 +
27 + TEST_METHOD_SETUP(MethodSetup)
28 + {
29 + EnsureVolumeDoesNotExist(TestVolumeName1);
30 + EnsureVolumeDoesNotExist(TestVolumeName2);
31 + return true;
32 + }
33 +
34 + TEST_CLASS_CLEANUP(ClassCleanup)
35 + {
36 + EnsureVolumeDoesNotExist(TestVolumeName1);
37 + EnsureVolumeDoesNotExist(TestVolumeName2);
38 + return true;
39 + }
40 +
41 + WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_HelpCommand)
42 + {
43 + auto result = RunWslc(L"volume inspect --help");
44 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
45 + }
46 +
47 + WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_MissingVolumeName)
48 + {
49 + auto result = RunWslc(L"volume inspect");
50 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'volume-name'\r\n", .ExitCode = 1});
51 + }
52 +
53 + WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_Success)
54 + {
55 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName1));
56 + result.Verify({.Stderr = L"", .ExitCode = 0});
57 + VERIFY_ARE_EQUAL(TestVolumeName1, result.GetStdoutOneLine());
58 +
59 + result = RunWslc(std::format(L"volume inspect {}", TestVolumeName1));
60 + result.Verify({.Stderr = L"", .ExitCode = 0});
61 + auto inspectData =
62 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectVolume>>(result.Stdout.value().c_str());
63 + VERIFY_ARE_EQUAL(1u, inspectData.size());
64 + auto inspect = inspectData[0];
65 +
66 + VERIFY_ARE_EQUAL(WideToMultiByte(TestVolumeName1), inspect.Name);
67 + VERIFY_ARE_EQUAL("guest", inspect.Driver);
68 + }
69 +
70 + WSLC_TEST_METHOD(WSLCE2E_Volume_InspectMultiple_Success)
71 + {
72 + // Create two volumes to inspect at the same time
73 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName1));
74 + result.Verify({.Stderr = L"", .ExitCode = 0});
75 + VERIFY_ARE_EQUAL(TestVolumeName1, result.GetStdoutOneLine());
76 + result = RunWslc(std::format(L"volume create {}", TestVolumeName2));
77 + result.Verify({.Stderr = L"", .ExitCode = 0});
78 + VERIFY_ARE_EQUAL(TestVolumeName2, result.GetStdoutOneLine());
79 +
80 + // Inspect both volumes in the same command
81 + result = RunWslc(std::format(L"volume inspect {} {}", TestVolumeName1, TestVolumeName2));
82 + result.Verify({.Stderr = L"", .ExitCode = 0});
83 + auto inspectData =
84 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectVolume>>(result.Stdout.value().c_str());
85 + VERIFY_ARE_EQUAL(2u, inspectData.size());
86 +
87 + auto inspect1 = inspectData[0];
88 + VERIFY_ARE_EQUAL(WideToMultiByte(TestVolumeName1), inspect1.Name);
89 + VERIFY_ARE_EQUAL("guest", inspect1.Driver);
90 +
91 + auto inspect2 = inspectData[1];
92 + VERIFY_ARE_EQUAL(WideToMultiByte(TestVolumeName2), inspect2.Name);
93 + VERIFY_ARE_EQUAL("guest", inspect2.Driver);
94 + }
95 +
96 + WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_NotFound)
97 + {
98 + auto result = RunWslc(std::format(L"volume inspect {}", TestVolumeName1));
99 + result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Volume not found: '{}'\r\n", TestVolumeName1), .ExitCode = 1});
100 + }
101 +
102 + WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_MixedFoundNotFound)
103 + {
104 + // Create one volume but not the other
105 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName1));
106 + result.Verify({.Stderr = L"", .ExitCode = 0});
107 + VERIFY_ARE_EQUAL(TestVolumeName1, result.GetStdoutOneLine());
108 +
109 + // Inspect both volumes in the same command, expecting one to be found and the other to not be found
110 + result = RunWslc(std::format(L"volume inspect {} {}", TestVolumeName1, TestVolumeName2));
111 + result.Verify({.Stderr = std::format(L"Volume not found: '{}'\r\n", TestVolumeName2), .ExitCode = 1});
112 +
113 + // Verify found volume
114 + auto inspectData =
115 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectVolume>>(result.Stdout.value().c_str());
116 + VERIFY_ARE_EQUAL(1u, inspectData.size());
117 + auto inspect = inspectData[0];
118 + VERIFY_ARE_EQUAL(WideToMultiByte(TestVolumeName1), inspect.Name);
119 + }
120 +
121 +private:
122 + const std::wstring TestVolumeName1 = L"wslc-e2e-volume-inspect-1";
123 + const std::wstring TestVolumeName2 = L"wslc-e2e-volume-inspect-2";
124 +
125 + std::wstring GetHelpMessage() const
126 + {
127 + std::wstringstream output;
128 + output << GetWslcHeader() //
129 + << GetDescription() //
130 + << GetUsage() //
131 + << GetAvailableCommands() //
132 + << GetAvailableOptions();
133 + return output.str();
134 + }
135 +
136 + std::wstring GetDescription() const
137 + {
138 + return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_VolumeInspectLongDesc());
139 + }
140 +
141 + std::wstring GetUsage() const
142 + {
143 + return L"Usage: wslc volume inspect [<options>] <volume-name>\r\n\r\n";
144 + }
145 +
146 + std::wstring GetAvailableCommands() const
147 + {
148 + std::wstringstream commands;
149 + commands << L"The following arguments are available:\r\n" //
150 + << L" volume-name Volume name\r\n" //
151 + << L"\r\n";
152 + return commands.str();
153 + }
154 +
155 + std::wstring GetAvailableOptions() const
156 + {
157 + std::wstringstream options;
158 + options << L"The following options are available:\r\n" //
159 + << L" --session Specify the session to use\r\n" //
160 + << L" -?,--help Shows help about the selected command\r\n" //
161 + << L"\r\n";
162 + return options.str();
163 + }
164 +};
165 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp new
+136
@@ -0,0 +1,136 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EVolumeListTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "VolumeModel.h"
17 +#include "WSLCExecutor.h"
18 +#include "WSLCE2EHelpers.h"
19 +
20 +namespace WSLCE2ETests {
21 +using namespace wsl::shared;
22 +using namespace wsl::shared::string;
23 +using namespace wsl::windows::wslc::models;
24 +
25 +class WSLCE2EVolumeListTests
26 +{
27 + WSLC_TEST_CLASS(WSLCE2EVolumeListTests)
28 +
29 + TEST_METHOD_SETUP(MethodSetup)
30 + {
31 + EnsureVolumeDoesNotExist(TestVolumeName);
32 + EnsureVolumeDoesNotExist(TestVolumeName2);
33 + return true;
34 + }
35 +
36 + TEST_CLASS_CLEANUP(ClassCleanup)
37 + {
38 + EnsureVolumeDoesNotExist(TestVolumeName);
39 + EnsureVolumeDoesNotExist(TestVolumeName2);
40 + return true;
41 + }
42 +
43 + WSLC_TEST_METHOD(WSLCE2E_Volume_List_HelpCommand)
44 + {
45 + auto result = RunWslc(L"volume list --help");
46 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
47 + }
48 +
49 + WSLC_TEST_METHOD(WSLCE2E_Volume_List_InvalidFormatOption)
50 + {
51 + auto result = RunWslc(L"volume list --format invalid");
52 + result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1});
53 + }
54 +
55 + WSLC_TEST_METHOD(WSLCE2E_Volume_List_QuietOption_OutputsNamesOnly)
56 + {
57 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
58 + result.Verify({.Stderr = L"", .ExitCode = 0});
59 + result = RunWslc(std::format(L"volume create {}", TestVolumeName2));
60 + result.Verify({.Stderr = L"", .ExitCode = 0});
61 +
62 + result = RunWslc(L"volume list --quiet");
63 + result.Verify({.Stderr = L"", .ExitCode = 0});
64 +
65 + auto lines = result.GetStdoutLines();
66 + VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), TestVolumeName));
67 + VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), TestVolumeName2));
68 + }
69 +
70 + WSLC_TEST_METHOD(WSLCE2E_Volume_List_JsonFormat)
71 + {
72 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
73 + result.Verify({.Stderr = L"", .ExitCode = 0});
74 + result = RunWslc(std::format(L"volume create {}", TestVolumeName2));
75 + result.Verify({.Stderr = L"", .ExitCode = 0});
76 +
77 + result = RunWslc(L"volume list --format json");
78 + result.Verify({.Stderr = L"", .ExitCode = 0});
79 +
80 + auto volumes = FromJson<std::vector<WSLCVolumeInformation>>(result.Stdout.value().c_str());
81 + VERIFY_ARE_EQUAL(2U, volumes.size());
82 +
83 + std::vector<std::string> names;
84 + names.reserve(volumes.size());
85 + for (const auto& volume : volumes)
86 + {
87 + names.push_back(volume.Name);
88 + }
89 +
90 + VERIFY_ARE_NOT_EQUAL(names.end(), std::find(names.begin(), names.end(), WideToMultiByte(TestVolumeName)));
91 + VERIFY_ARE_NOT_EQUAL(names.end(), std::find(names.begin(), names.end(), WideToMultiByte(TestVolumeName2)));
92 + }
93 +
94 +private:
95 + const std::wstring TestVolumeName = L"wslc-e2e-volume-list";
96 + const std::wstring TestVolumeName2 = L"wslc-e2e-volume-list-2";
97 +
98 + std::wstring GetHelpMessage() const
99 + {
100 + std::wstringstream output;
101 + output << GetWslcHeader() //
102 + << GetDescription() //
103 + << GetUsage() //
104 + << GetAvailableCommandAliases() //
105 + << GetAvailableOptions();
106 + return output.str();
107 + }
108 +
109 + std::wstring GetDescription() const
110 + {
111 + return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_VolumeListLongDesc());
112 + }
113 +
114 + std::wstring GetUsage() const
115 + {
116 + return L"Usage: wslc volume list [<options>]\r\n\r\n";
117 + }
118 +
119 + std::wstring GetAvailableCommandAliases() const
120 + {
121 + return L"The following command aliases are available: ls\r\n\r\n";
122 + }
123 +
124 + std::wstring GetAvailableOptions() const
125 + {
126 + std::wstringstream options;
127 + options << L"The following options are available:\r\n" //
128 + << L" --format Output formatting (json or table) (Default: table)\r\n"
129 + << L" -q,--quiet Outputs the volume names only\r\n" //
130 + << L" --session Specify the session to use\r\n" //
131 + << L" -?,--help Shows help about the selected command\r\n" //
132 + << L"\r\n";
133 + return options.str();
134 + }
135 +};
136 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeRemoveTests.cpp new
+186
@@ -0,0 +1,186 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EVolumeRemoveTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21 +
22 +class WSLCE2EVolumeRemoveTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EVolumeRemoveTests)
25 +
26 + TEST_CLASS_SETUP(ClassSetup)
27 + {
28 + EnsureImageIsLoaded(DebianImage);
29 + return true;
30 + }
31 +
32 + TEST_METHOD_SETUP(MethodSetup)
33 + {
34 + EnsureContainerDoesNotExist(WslcContainerName);
35 + EnsureVolumeDoesNotExist(TestVolumeName);
36 + EnsureVolumeDoesNotExist(TestVolumeName2);
37 + return true;
38 + }
39 +
40 + TEST_CLASS_CLEANUP(ClassCleanup)
41 + {
42 + EnsureContainerDoesNotExist(WslcContainerName);
43 + EnsureImageIsDeleted(DebianImage);
44 + EnsureVolumeDoesNotExist(TestVolumeName);
45 + EnsureVolumeDoesNotExist(TestVolumeName2);
46 + return true;
47 + }
48 +
49 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_HelpCommand)
50 + {
51 + auto result = RunWslc(L"volume remove --help");
52 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
53 + }
54 +
55 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_MissingVolumeName)
56 + {
57 + auto result = RunWslc(L"volume remove");
58 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'volume-name'\r\n", .ExitCode = 1});
59 + }
60 +
61 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_Valid)
62 + {
63 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
64 + result.Verify({.Stderr = L"", .ExitCode = 0});
65 +
66 + VerifyVolumeIsListed(TestVolumeName);
67 +
68 + result = RunWslc(std::format(L"volume remove {}", TestVolumeName));
69 + result.Verify({.Stdout = std::format(L"{}\r\n", TestVolumeName), .Stderr = L"", .ExitCode = 0});
70 +
71 + VerifyVolumeIsNotListed(TestVolumeName);
72 + }
73 +
74 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_Multiple_Valid)
75 + {
76 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
77 + result.Verify({.Stderr = L"", .ExitCode = 0});
78 + result = RunWslc(std::format(L"volume create {}", TestVolumeName2));
79 + result.Verify({.Stderr = L"", .ExitCode = 0});
80 +
81 + VerifyVolumeIsListed(TestVolumeName);
82 + VerifyVolumeIsListed(TestVolumeName2);
83 +
84 + result = RunWslc(std::format(L"volume remove {} {}", TestVolumeName, TestVolumeName2));
85 + result.Verify({.Stderr = L"", .ExitCode = 0});
86 +
87 + VerifyVolumeIsNotListed(TestVolumeName);
88 + VerifyVolumeIsNotListed(TestVolumeName2);
89 + }
90 +
91 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_NotFound)
92 + {
93 + auto result = RunWslc(std::format(L"volume remove {}", TestVolumeName));
94 + result.Verify({.Stdout = L"", .Stderr = std::format(L"Volume not found: '{}'\r\n", TestVolumeName), .ExitCode = 1});
95 + }
96 +
97 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_MixedFoundNotFound)
98 + {
99 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
100 + result.Verify({.Stderr = L"", .ExitCode = 0});
101 + VerifyVolumeIsListed(TestVolumeName);
102 +
103 + result = RunWslc(std::format(L"volume remove {} {}", TestVolumeName, TestVolumeName2));
104 + result.Verify(
105 + {.Stdout = std::format(L"{}\r\n", TestVolumeName), .Stderr = std::format(L"Volume not found: '{}'\r\n", TestVolumeName2), .ExitCode = 1});
106 + VerifyVolumeIsNotListed(TestVolumeName);
107 + }
108 +
109 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_VolumeInUse_Fail)
110 + {
111 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
112 + result.Verify({.Stderr = L"", .ExitCode = 0});
113 + VerifyVolumeIsListed(TestVolumeName);
114 +
115 + // Create a container that uses the volume to ensure it's in use
116 + result = RunWslc(std::format(
117 + L"container run -d --name {} -v {}:/data {} sh -c \"echo -n 'WSLC Volume In Use Test' > /data/test.txt && sleep "
118 + L"infinity\"",
119 + WslcContainerName,
120 + TestVolumeName,
121 + DebianImage.NameAndTag()));
122 + result.Verify({.Stderr = L"", .ExitCode = 0});
123 +
124 + // Attempt to remove the volume while it's in use
125 + result = RunWslc(std::format(L"volume remove {}", TestVolumeName));
126 + result.Verify(
127 + {.Stdout = L"",
128 + .Stderr = std::format(L"Volume '{}' is in use.\r\nError code: ERROR_SHARING_VIOLATION\r\n", TestVolumeName),
129 + .ExitCode = 1});
130 +
131 + VerifyVolumeIsListed(TestVolumeName);
132 + }
133 +
134 +private:
135 + const std::wstring WslcContainerName = L"wslc-test-container";
136 + const TestImage& DebianImage = DebianTestImage();
137 + const std::wstring TestVolumeName = L"wslc-e2e-volume-remove";
138 + const std::wstring TestVolumeName2 = L"wslc-e2e-volume-remove-2";
139 +
140 + std::wstring GetHelpMessage() const
141 + {
142 + std::wstringstream output;
143 + output << GetWslcHeader() //
144 + << GetDescription() //
145 + << GetUsage() //
146 + << GetAvailableCommandAliases() //
147 + << GetAvailableCommands() //
148 + << GetAvailableOptions();
149 + return output.str();
150 + }
151 +
152 + std::wstring GetDescription() const
153 + {
154 + return Localization::WSLCCLI_VolumeRemoveLongDesc() + L"\r\n\r\n";
155 + }
156 +
157 + std::wstring GetUsage() const
158 + {
159 + return L"Usage: wslc volume remove [<options>] <volume-name>\r\n\r\n";
160 + }
161 +
162 + std::wstring GetAvailableCommandAliases() const
163 + {
164 + return L"The following command aliases are available: delete rm\r\n\r\n";
165 + }
166 +
167 + std::wstring GetAvailableCommands() const
168 + {
169 + std::wstringstream commands;
170 + commands << L"The following arguments are available:\r\n" //
171 + << L" volume-name Volume name\r\n" //
172 + << L"\r\n";
173 + return commands.str();
174 + }
175 +
176 + std::wstring GetAvailableOptions() const
177 + {
178 + std::wstringstream options;
179 + options << L"The following options are available:\r\n" //
180 + << L" --session Specify the session to use\r\n" //
181 + << L" -?,--help Shows help about the selected command\r\n" //
182 + << L"\r\n";
183 + return options.str();
184 + }
185 +};
186 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp new
+101
@@ -0,0 +1,101 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EVolumeTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +#include "Argument.h"
19 +
20 +namespace WSLCE2ETests {
21 +using namespace wsl::shared;
22 +
23 +class WSLCE2EVolumeTests
24 +{
25 + WSLC_TEST_CLASS(WSLCE2EVolumeTests)
26 +
27 + WSLC_TEST_METHOD(WSLCE2E_Volume_HelpCommand)
28 + {
29 + auto result = RunWslc(L"volume --help");
30 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
31 + }
32 +
33 + WSLC_TEST_METHOD(WSLCE2E_Volume_NoSubcommand_ShowsHelp)
34 + {
35 + auto result = RunWslc(L"volume");
36 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
37 + }
38 +
39 + WSLC_TEST_METHOD(WSLCE2E_Volume_InvalidCommand_DisplaysErrorMessage)
40 + {
41 + auto result = RunWslc(L"volume INVALID_CMD");
42 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
43 + }
44 +
45 +private:
46 + std::wstring GetHelpMessage() const
47 + {
48 + std::wstringstream output;
49 + output << GetWslcHeader() //
50 + << GetDescription() //
51 + << GetUsage() //
52 + << GetAvailableCommands() //
53 + << GetAvailableOptions();
54 + return output.str();
55 + }
56 +
57 + std::wstring GetDescription() const
58 + {
59 + return Localization::WSLCCLI_VolumeCommandLongDesc() + L"\r\n\r\n";
60 + }
61 +
62 + std::wstring GetUsage() const
63 + {
64 + return L"Usage: wslc volume [<command>] [<options>]\r\n\r\n";
65 + }
66 +
67 + std::wstring GetAvailableCommands() const
68 + {
69 + std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
70 + {L"create", Localization::WSLCCLI_VolumeCreateDesc()},
71 + {L"remove", Localization::WSLCCLI_VolumeRemoveDesc()},
72 + {L"inspect", Localization::WSLCCLI_VolumeInspectDesc()},
73 + {L"list", Localization::WSLCCLI_VolumeListDesc()},
74 + };
75 +
76 + size_t maxLen = 0;
77 + for (const auto& [name, _] : entries)
78 + {
79 + maxLen = (std::max)(maxLen, name.size());
80 + }
81 +
82 + std::wstringstream commands;
83 + commands << Localization::WSLCCLI_AvailableSubcommands() << L"\r\n";
84 + for (const auto& [name, desc] : entries)
85 + {
86 + commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
87 + }
88 + commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n\r\n";
89 + return commands.str();
90 + }
91 +
92 + std::wstring GetAvailableOptions() const
93 + {
94 + std::wstringstream options;
95 + options << L"The following options are available:\r\n"
96 + << L" -?,--help Shows help about the selected command\r\n"
97 + << L"\r\n";
98 + return options.str();
99 + }
100 +};
101 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCExecutor.cpp new
+456
@@ -0,0 +1,456 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCExecutor.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the implementation of the WSLCExecutor class, which is
12 + responsible for executing wslc commands and verifying their results in
13 + end-to-end tests.
14 +--*/
15 +
16 +#include "precomp.h"
17 +#include "windows/Common.h"
18 +#include "WSLCExecutor.h"
19 +#include "WSLCE2EHelpers.h"
20 +
21 +namespace WSLCE2ETests {
22 +
23 +using namespace WEX::Logging;
24 +using namespace wsl::windows::common;
25 +
26 +namespace {
27 + wil::unique_handle GetNonElevatedPrimaryToken()
28 + {
29 + // This method is necessary because GetNonElevatedToken(TokenPrimary) does
30 + // not actually give a de-elevated token when called from an elevated process.
31 + // By getting impersonation token first this de-elevates the token, and then
32 + // converts it to a primary token.
33 + auto impersonationToken = GetNonElevatedToken(TokenImpersonation);
34 + wil::unique_handle primaryToken;
35 + THROW_IF_WIN32_BOOL_FALSE(
36 + DuplicateTokenEx(impersonationToken.get(), TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, TokenPrimary, &primaryToken));
37 +
38 + VERIFY_IS_FALSE(wsl::windows::common::security::IsTokenElevated(primaryToken.get()));
39 + return primaryToken;
40 + }
41 +} // namespace
42 +
43 +void WSLCExecutionResult::Dump(bool escapeStrings) const
44 +{
45 + Log::Comment((L"Command Line: \"" + CommandLine + L"\"").c_str());
46 + if (Stdout)
47 + {
48 + if (escapeStrings)
49 + {
50 + std::string stdoutStr = wsl::windows::common::string::WideToMultiByte(*Stdout);
51 + std::string escapedStdout = EscapeString(stdoutStr);
52 + Log::Comment(std::format(L"Stdout: \"{}\"", wsl::shared::string::MultiByteToWide(escapedStdout)).c_str());
53 + }
54 + else
55 + {
56 + Log::Comment((L"Stdout: \"" + *Stdout + L"\"").c_str());
57 + }
58 + }
59 +
60 + if (Stderr)
61 + {
62 + if (escapeStrings)
63 + {
64 + std::string stderrStr = wsl::windows::common::string::WideToMultiByte(*Stderr);
65 + std::string escapedStderr = EscapeString(stderrStr);
66 + Log::Comment(std::format(L"Stderr (escaped): \"{}\"", wsl::shared::string::MultiByteToWide(escapedStderr)).c_str());
67 + }
68 + else
69 + {
70 + Log::Comment((L"Stderr: \"" + *Stderr + L"\"").c_str());
71 + }
72 + }
73 +
74 + if (ExitCode)
75 + {
76 + Log::Comment((L"Exit Code: " + std::to_wstring(*ExitCode)).c_str());
77 + }
78 +}
79 +
80 +void WSLCExecutionResult::Verify(const WSLCExecutionResult& expected) const
81 +{
82 + if (expected.Stdout)
83 + {
84 + VERIFY_ARE_EQUAL(*expected.Stdout, *Stdout);
85 + }
86 +
87 + if (expected.Stderr)
88 + {
89 + VERIFY_ARE_EQUAL(*expected.Stderr, *Stderr);
90 + }
91 +
92 + if (expected.ExitCode)
93 + {
94 + VERIFY_ARE_EQUAL(*expected.ExitCode, *ExitCode);
95 + }
96 +}
97 +
98 +std::vector<std::wstring> WSLCExecutionResult::GetStdoutLines() const
99 +{
100 + std::vector<std::wstring> lines;
101 + std::wstringstream ss(*Stdout);
102 + std::wstring line;
103 + while (std::getline(ss, line))
104 + {
105 + // Remove carriage return if present
106 + if (!line.empty() && line.back() == L'\r')
107 + {
108 + line.pop_back();
109 + }
110 +
111 + lines.push_back(line);
112 + }
113 + return lines;
114 +}
115 +
116 +std::wstring WSLCExecutionResult::GetStdoutOneLine() const
117 +{
118 + auto stdoutLines = GetStdoutLines();
119 +
120 + // Remove empty trailing lines (common when output ends with \n)
121 + while (!stdoutLines.empty() && stdoutLines.back().empty())
122 + {
123 + stdoutLines.pop_back();
124 + }
125 +
126 + VERIFY_ARE_EQUAL(1u, stdoutLines.size());
127 + return stdoutLines[0];
128 +}
129 +
130 +bool WSLCExecutionResult::StdoutContainsLine(const std::wstring& expectedLine) const
131 +{
132 + VERIFY_IS_TRUE(Stdout.has_value());
133 + for (const auto& line : GetStdoutLines())
134 + {
135 + if (line == expectedLine)
136 + {
137 + return true;
138 + }
139 + }
140 +
141 + return false;
142 +}
143 +
144 +WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType)
145 +{
146 + auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
147 + wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
148 +
149 + // If running non-elevated we need to keep the token alive until it completes.
150 + wil::unique_handle nonElevatedToken;
151 + if (elevationType == ElevationType::NonElevated)
152 + {
153 + nonElevatedToken = GetNonElevatedPrimaryToken();
154 + process.SetToken(nonElevatedToken.get());
155 + }
156 +
157 + const auto output = process.RunAndCaptureOutput();
158 + return {.CommandLine = commandLine, .Stdout = output.Stdout, .Stderr = output.Stderr, .ExitCode = output.ExitCode};
159 +}
160 +
161 +void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expected, ElevationType elevationType)
162 +{
163 + RunWslc(cmd, elevationType).Verify(expected);
164 +}
165 +
166 +WSLCExecutionResult RunWslcAndRedirectToFile(const std::wstring& commandLine, std::optional<std::filesystem::path> outputPath, ElevationType elevationType)
167 +{
168 + auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
169 + wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
170 +
171 + // If running non-elevated we need to keep the token alive until it completes.
172 + wil::unique_handle nonElevatedToken;
173 + if (elevationType == ElevationType::NonElevated)
174 + {
175 + nonElevatedToken = GetNonElevatedPrimaryToken();
176 + process.SetToken(nonElevatedToken.get());
177 + }
178 +
179 + auto [parentStderrRead, childStderrWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, true, false);
180 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStderrWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
181 +
182 + wil::unique_hfile redirectedStdout;
183 + HANDLE stdoutHandle = nullptr;
184 +
185 + std::wstring effectiveCommandLine = commandLine;
186 + if (outputPath.has_value())
187 + {
188 + SECURITY_ATTRIBUTES securityAttributes{};
189 + securityAttributes.nLength = sizeof(securityAttributes);
190 + securityAttributes.bInheritHandle = TRUE;
191 + redirectedStdout.reset(CreateFileW(
192 + outputPath->c_str(), GENERIC_WRITE, FILE_SHARE_READ, &securityAttributes, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
193 + THROW_LAST_ERROR_IF(!redirectedStdout);
194 + stdoutHandle = redirectedStdout.get();
195 + effectiveCommandLine = std::format(L"{} > \"{}\"", commandLine, outputPath->wstring());
196 + }
197 + else
198 + {
199 + // Open CONOUT$ so the child process receives a real console handle regardless of
200 + // how the test runner has configured its own stdout (e.g. piped in CI). This
201 + // makes IsConsoleHandle() return true inside wslc, which is the condition under
202 + // test in WSLCE2E_Image_Save_ToTerminal_Fail.
203 + SECURITY_ATTRIBUTES securityAttributes{};
204 + securityAttributes.nLength = sizeof(securityAttributes);
205 + securityAttributes.bInheritHandle = TRUE;
206 + redirectedStdout.reset(
207 + CreateFileW(L"CONOUT$", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &securityAttributes, OPEN_EXISTING, 0, nullptr));
208 + THROW_LAST_ERROR_IF(!redirectedStdout);
209 + stdoutHandle = redirectedStdout.get();
210 + }
211 +
212 + process.SetStdHandles(nullptr, stdoutHandle, childStderrWrite.get());
213 +
214 + const auto processHandle = process.Start();
215 + childStderrWrite.reset();
216 +
217 + const auto exitCode = wsl::windows::common::SubProcess::GetExitCode(processHandle.get());
218 + const auto stdErrOutput = wsl::shared::string::MultiByteToWide(ReadToString(parentStderrRead.get()));
219 +
220 + return {.CommandLine = std::move(effectiveCommandLine), .Stdout = L"", .Stderr = stdErrOutput, .ExitCode = exitCode};
221 +}
222 +
223 +std::wstring GetWslcHeader()
224 +{
225 + std::wstringstream header;
226 + header << L"Copyright (c) Microsoft Corporation. All rights reserved.\r\n"
227 + << L"For privacy information about this product please visit https://aka.ms/privacy.\r\n"
228 + << L"\r\n";
229 + return header.str();
230 +}
231 +
232 +WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType)
233 +{
234 + auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
235 +
236 + auto [childStdinRead, parentStdinWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, false, true);
237 + auto [parentStdoutRead, childStdoutWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false);
238 + auto [parentStderrRead, childStderrWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false);
239 +
240 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdinRead.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
241 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
242 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStderrWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
243 +
244 + wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
245 + process.SetStdHandles(childStdinRead.get(), childStdoutWrite.get(), childStderrWrite.get());
246 +
247 + wil::unique_handle nonElevatedToken;
248 + if (elevationType == ElevationType::NonElevated)
249 + {
250 + nonElevatedToken = GetNonElevatedPrimaryToken();
251 + process.SetToken(nonElevatedToken.get());
252 + }
253 +
254 + wil::unique_handle processHandle = process.Start();
255 +
256 + childStdinRead.reset();
257 + childStdoutWrite.reset();
258 + childStderrWrite.reset();
259 +
260 + return WSLCInteractiveSession(
261 + commandLine,
262 + std::move(parentStdinWrite),
263 + std::move(parentStdoutRead),
264 + std::move(parentStderrRead),
265 + std::move(processHandle),
266 + std::move(nonElevatedToken)); // Transfer token ownership to the session
267 +}
268 +
269 +// WSLCInteractiveSession implementation
270 +
271 +WSLCInteractiveSession::WSLCInteractiveSession(
272 + std::wstring commandLine,
273 + wil::unique_hfile stdinWrite,
274 + wil::unique_hfile stdoutRead,
275 + wil::unique_hfile stderrRead,
276 + wil::unique_handle processHandle,
277 + wil::unique_handle nonElevatedToken) :
278 + CommandLine(std::move(commandLine)),
279 + m_stdinWrite(std::move(stdinWrite)),
280 + m_stdoutRead(std::move(stdoutRead)),
281 + m_stderrRead(std::move(stderrRead)),
282 + m_processHandle(std::move(processHandle)),
283 + m_nonElevatedToken(std::move(nonElevatedToken))
284 +{
285 + m_stdoutReader = std::make_unique<PartialHandleRead>(m_stdoutRead.get());
286 + m_stderrReader = std::make_unique<PartialHandleRead>(m_stderrRead.get());
287 +}
288 +
289 +WSLCInteractiveSession::~WSLCInteractiveSession()
290 +{
291 + // Best-effort cleanup to avoid orphaned wslc process if Exit()/Wait() were not called.
292 + if (!m_processHandle.is_valid())
293 + {
294 + return;
295 + }
296 +
297 + CloseStdin();
298 +
299 + DWORD waitResult = ::WaitForSingleObject(m_processHandle.get(), DefaultWaitTimeoutMs);
300 + if (waitResult == WAIT_TIMEOUT)
301 + {
302 + // Still running: terminate and wait again, but do not throw.
303 + ::TerminateProcess(m_processHandle.get(), 1);
304 + ::WaitForSingleObject(m_processHandle.get(), DefaultWaitTimeoutMs);
305 + }
306 +}
307 +
308 +void WSLCInteractiveSession::ExpectStdout(const std::string& expected)
309 +{
310 + Log::Comment(std::format(L"Expecting stdout: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(expected))).c_str());
311 + m_stdoutReader->ExpectConsume(expected);
312 +}
313 +
314 +void WSLCInteractiveSession::ExpectStderr(const std::string& expected)
315 +{
316 + Log::Comment(std::format(L"Expecting stderr: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(expected))).c_str());
317 + m_stderrReader->ExpectConsume(expected);
318 +}
319 +
320 +void WSLCInteractiveSession::ExpectCommandEcho(const std::string& command)
321 +{
322 + // TTY mode: expect command echo, then B_END and carriage return
323 + ExpectStdout(std::format("{}\r\n{}\r", command, VT::B_END));
324 +}
325 +
326 +void WSLCInteractiveSession::Write(const std::string& data)
327 +{
328 + Log::Comment(std::format(L"Writing to stdin: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(data))).c_str());
329 +
330 + OVERLAPPED overlapped{};
331 + wil::unique_event event(wil::EventOptions::ManualReset);
332 + overlapped.hEvent = event.get();
333 +
334 + DWORD written = 0;
335 + if (!WriteFile(m_stdinWrite.get(), data.c_str(), static_cast<DWORD>(data.size()), &written, &overlapped))
336 + {
337 + DWORD error = GetLastError();
338 + if (error == ERROR_IO_PENDING)
339 + {
340 + DWORD waitResult = WaitForSingleObject(event.get(), DefaultWaitTimeoutMs);
341 + if (waitResult == WAIT_TIMEOUT)
342 + {
343 + THROW_HR(HRESULT_FROM_WIN32(ERROR_TIMEOUT));
344 + }
345 + else if (waitResult == WAIT_FAILED)
346 + {
347 + THROW_LAST_ERROR();
348 + }
349 + else if (waitResult != WAIT_OBJECT_0)
350 + {
351 + THROW_HR_MSG(E_UNEXPECTED, "WaitForSingleObject returned unexpected result: 0x%08lx", waitResult);
352 + }
353 +
354 + THROW_IF_WIN32_BOOL_FALSE(GetOverlappedResult(m_stdinWrite.get(), &overlapped, &written, FALSE));
355 + }
356 + else
357 + {
358 + THROW_WIN32(error);
359 + }
360 + }
361 +}
362 +
363 +void WSLCInteractiveSession::WriteLine(const std::string& line)
364 +{
365 + Write(line + "\n");
366 +}
367 +
368 +bool WSLCInteractiveSession::IsRunning() const
369 +{
370 + DWORD exitCode = 0;
371 + return GetExitCodeProcess(m_processHandle.get(), &exitCode) && exitCode == STILL_ACTIVE;
372 +}
373 +
374 +void WSLCInteractiveSession::CloseStdin()
375 +{
376 + m_stdinWrite.reset();
377 +}
378 +
379 +std::optional<int> WSLCInteractiveSession::GetExitCode() const
380 +{
381 + DWORD exitCode = 0;
382 + if (GetExitCodeProcess(m_processHandle.get(), &exitCode) && exitCode != STILL_ACTIVE)
383 + {
384 + return static_cast<int>(exitCode);
385 + }
386 +
387 + return std::nullopt;
388 +}
389 +
390 +void WSLCInteractiveSession::WaitForExit(DWORD timeoutMs)
391 +{
392 + auto result = WaitForSingleObject(m_processHandle.get(), timeoutMs);
393 + if (result == WAIT_TIMEOUT)
394 + {
395 + DWORD processId = GetProcessId(m_processHandle.get());
396 +
397 + Log::Warning(std::format(L"Process (PID: {}) did not exit within timeout of {}ms", processId, timeoutMs).c_str());
398 + Log::Warning(L"Attempting to terminate process forcefully");
399 + Terminate(999);
400 + WaitForSingleObject(m_processHandle.get(), DefaultWaitTimeoutMs);
401 +
402 + THROW_HR_MSG(E_FAIL, "Process did not exit within timeout of %lums and was forcefully terminated", timeoutMs);
403 + }
404 +
405 + if (result == WAIT_FAILED)
406 + {
407 + THROW_LAST_ERROR_MSG("WaitForSingleObject failed while waiting for process exit");
408 + }
409 +
410 + if (result != WAIT_OBJECT_0)
411 + {
412 + THROW_HR_MSG(E_UNEXPECTED, "WaitForSingleObject returned unexpected result: 0x%08lx", result);
413 + }
414 +}
415 +
416 +int WSLCInteractiveSession::Wait(DWORD timeoutMs)
417 +{
418 + WaitForExit(timeoutMs);
419 + DWORD exitCode = 0;
420 + THROW_IF_WIN32_BOOL_FALSE(GetExitCodeProcess(m_processHandle.get(), &exitCode));
421 + return static_cast<int>(exitCode);
422 +}
423 +
424 +bool WSLCInteractiveSession::Terminate(UINT exitCode)
425 +{
426 + return TerminateProcess(m_processHandle.get(), exitCode) != FALSE;
427 +}
428 +
429 +void WSLCInteractiveSession::VerifyNoErrors()
430 +{
431 + m_stderrReader->ExpectClosed(DefaultWaitTimeoutMs);
432 +
433 + // Verify that stderr was actually empty - not just closed
434 + const auto& stderrContent = m_stderrReader->GetData();
435 + if (!stderrContent.empty())
436 + {
437 + VERIFY_FAIL(std::format(L"Expected no errors but stderr contained: {}", wsl::shared::string::MultiByteToWide(EscapeString(stderrContent)))
438 + .c_str());
439 + }
440 +}
441 +
442 +int WSLCInteractiveSession::Exit(DWORD timeoutMs)
443 +{
444 + WriteLine("exit");
445 + CloseStdin();
446 + return Wait(timeoutMs);
447 +}
448 +
449 +int WSLCInteractiveSession::ExitAndVerifyNoErrors(DWORD timeoutMs)
450 +{
451 + const auto exitCode = Exit(timeoutMs);
452 + VerifyNoErrors();
453 + return exitCode;
454 +}
455 +
456 +} // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCExecutor.h new
+106
@@ -0,0 +1,106 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCExecutor.h
8 +
9 +Abstract:
10 +
11 + This file contains the declaration of the WSLCExecutor class, which
12 + provides functionality to execute wslc commands and verify their results in
13 + end-to-end tests.
14 +--*/
15 +
16 +#pragma once
17 +
18 +#include "precomp.h"
19 +#include "windows/Common.h"
20 +
21 +namespace WSLCE2ETests {
22 +
23 +constexpr DWORD DefaultWaitTimeoutMs = 60000; // 60 seconds
24 +
25 +enum class ElevationType
26 +{
27 + Elevated,
28 + NonElevated
29 +};
30 +
31 +inline std::wstring GetWslcPath()
32 +{
33 + return (std::filesystem::path(wsl::windows::common::wslutil::GetMsiPackagePath().value()) / L"wslc.exe").wstring();
34 +}
35 +
36 +struct WSLCExecutionResult
37 +{
38 + std::wstring CommandLine{};
39 + std::optional<std::wstring> Stdout{};
40 + std::optional<std::wstring> Stderr{};
41 + std::optional<DWORD> ExitCode{};
42 + void Dump(bool escapeStrings = false) const;
43 + void Verify(const WSLCExecutionResult& expected) const;
44 + std::vector<std::wstring> GetStdoutLines() const;
45 + std::wstring GetStdoutOneLine() const;
46 + bool StdoutContainsLine(const std::wstring& expectedLine) const;
47 +};
48 +
49 +// Interactive session for testing wslc commands that require stdin/stdout interaction.
50 +// Uses PartialHandleRead for race-free output validation
51 +struct WSLCInteractiveSession
52 +{
53 + WSLCInteractiveSession(
54 + std::wstring commandLine,
55 + wil::unique_hfile stdinWrite,
56 + wil::unique_hfile stdoutRead,
57 + wil::unique_hfile stderrRead,
58 + wil::unique_handle processHandle,
59 + wil::unique_handle nonElevatedToken = wil::unique_handle{});
60 + ~WSLCInteractiveSession();
61 +
62 + // Non-copyable, non-movable
63 + WSLCInteractiveSession(const WSLCInteractiveSession&) = delete;
64 + WSLCInteractiveSession& operator=(const WSLCInteractiveSession&) = delete;
65 + WSLCInteractiveSession(WSLCInteractiveSession&&) = delete;
66 + WSLCInteractiveSession& operator=(WSLCInteractiveSession&&) = delete;
67 +
68 + std::wstring CommandLine;
69 +
70 + void Write(const std::string& data);
71 + void WriteLine(const std::string& line);
72 + void ExpectStdout(const std::string& expected);
73 + void ExpectStderr(const std::string& expected);
74 + void ExpectCommandEcho(const std::string& command);
75 +
76 + bool IsRunning() const;
77 + void CloseStdin();
78 + std::optional<int> GetExitCode() const;
79 + void WaitForExit(DWORD timeoutMs = DefaultWaitTimeoutMs);
80 + int Wait(DWORD timeoutMs = DefaultWaitTimeoutMs);
81 + bool Terminate(UINT exitCode = 1);
82 + void VerifyNoErrors();
83 + int Exit(DWORD timeoutMs = DefaultWaitTimeoutMs);
84 + int ExitAndVerifyNoErrors(DWORD timeoutMs = DefaultWaitTimeoutMs);
85 +
86 +private:
87 + wil::unique_hfile m_stdinWrite;
88 + wil::unique_hfile m_stdoutRead;
89 + wil::unique_hfile m_stderrRead;
90 + wil::unique_handle m_processHandle;
91 + wil::unique_handle m_nonElevatedToken; // Keep token alive for the lifetime of the session
92 + std::unique_ptr<PartialHandleRead> m_stdoutReader;
93 + std::unique_ptr<PartialHandleRead> m_stderrReader;
94 +};
95 +
96 +WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated);
97 +WSLCExecutionResult RunWslcAndRedirectToFile(
98 + const std::wstring& commandLine,
99 + std::optional<std::filesystem::path> outputPath = std::nullopt,
100 + ElevationType elevationType = ElevationType::Elevated);
101 +void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expected, ElevationType elevationType = ElevationType::Elevated);
102 +
103 +std::wstring GetWslcHeader();
104 +WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated);
105 +
106 +} // namespace WSLCE2ETests
tools/hooks/pre-commit.in
+1 -1
@@ -13,7 +13,7 @@
13
14 MODE="@WSL_PRE_COMMIT_MODE@"
15 REPO_ROOT="$(git rev-parse --show-toplevel)"
16 -CLANG_FORMAT="$REPO_ROOT/tools/clang-format.exe"
16 +CLANG_FORMAT="@LLVM_INSTALL_DIR@/clang-format.exe"
17
18 # --- Collect staged C/C++ source files ---
19 STAGED=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(c|cpp|cxx|h|hpp|hxx)$')
tools/test/Microsoft.WSL.TestData.nuspec new
+15
@@ -0,0 +1,15 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
3 + <metadata>
4 + <id>Microsoft.WSL.TestData</id>
5 + <version>$version$</version>
6 + <authors>Microsoft</authors>
7 + <requireLicenseAcceptance>false</requireLicenseAcceptance>
8 + <projectUrl>https://github.com/microsoft/WSL</projectUrl>
9 + <description>WSL Test Data Files</description>
10 + </metadata>
11 + <files>
12 + <file src="x64\**\*.*" target="x64" />
13 + <file src="arm64\**\*.*" target="arm64" />
14 + </files>
15 +</package>
\ No newline at end of file
tools/test/images/build-image.ps1 new
+48
@@ -0,0 +1,48 @@
1 +<#
2 +.SYNOPSIS
3 + Builds a custom test registry image using wslc and saves it as a .tar file.
4 +.DESCRIPTION
5 + This script builds a custom image using wslc from a specified Dockerfile and saves the resulting image as a .tar file.
6 + This is useful for preparing test images for WSL container tests.
7 +.PARAMETER DockerfileDir
8 + Path to the directory containing the Dockerfile to build.
9 +.PARAMETER ImageTag
10 + Tag for the built image.
11 +.PARAMETER OutputFile
12 + Path to save the exported .tar file. Defaults to <DockerfileDir name>.tar in the current directory.
13 +#>
14 +
15 +[CmdletBinding(SupportsShouldProcess)]
16 +param (
17 + [string]$DockerfileDir,
18 + [string]$ImageTag,
19 + [string]$OutputFile = ""
20 +)
21 +
22 +$ErrorActionPreference = "Stop"
23 +Set-StrictMode -Version Latest
24 +
25 +if ($OutputFile -eq "") {
26 + $OutputFile = Join-Path $PWD "$(Split-Path -Leaf $DockerfileDir).tar"
27 +}
28 +
29 +# Verify $OutputFile is a valid path, we can write to it, and that it has a .tar extension
30 +if ([System.IO.Path]::GetExtension($OutputFile) -ne ".tar") {
31 + if (-not $PSCmdlet.ShouldContinue("Are you sure you want to continue?", "Output file '$OutputFile' is not a .tar file.")) {
32 + throw "Aborting due to invalid output file extension."
33 + }
34 +}
35 +
36 +
37 +if ($PSCmdlet.ShouldProcess($ImageTag, "Build image from '$DockerfileDir'")) {
38 + & wslc build -t $ImageTag $DockerfileDir
39 + if ($LASTEXITCODE -ne 0) { throw "wslc build failed with exit code $LASTEXITCODE" }
40 +}
41 +
42 +if ($PSCmdlet.ShouldProcess($OutputFile, "Save image '$ImageTag'")) {
43 + & wslc save --output $OutputFile $ImageTag
44 + if ($LASTEXITCODE -ne 0) { throw "wslc save failed with exit code $LASTEXITCODE" }
45 +
46 + Write-Host "Image built and saved to $OutputFile successfully."
47 +}
48 +
tools/test/images/wslc-registry/Dockerfile new
+8
@@ -0,0 +1,8 @@
1 +FROM registry:3
2 +
3 +RUN apk add --no-cache apache2-utils
4 +
5 +COPY entrypoint.sh /entrypoint.sh
6 +RUN chmod +x /entrypoint.sh
7 +
8 +ENTRYPOINT ["/entrypoint.sh"]
tools/test/images/wslc-registry/entrypoint.sh new
+13
@@ -0,0 +1,13 @@
1 +#!/bin/sh
2 +set -e
3 +
4 +if [ -n "$USERNAME" ]; then
5 + mkdir -p /auth
6 + htpasswd -Bbn "$USERNAME" "$PASSWORD" > /auth/htpasswd
7 +
8 + export REGISTRY_AUTH=htpasswd
9 + export REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
10 + export REGISTRY_AUTH_HTPASSWD_REALM="WSLC Registry"
11 +fi
12 +
13 +exec registry serve /etc/distribution/config.yml
tools/test/pack-test-data.ps1 new
+35
@@ -0,0 +1,35 @@
1 +<#
2 +.SYNOPSIS
3 + Helper to pack WSL test data nuget.
4 +.PARAMETER InputDirectory
5 + Directory containing arch-specific subdirectories (x64, arm64) with test data.
6 +.PARAMETER Version
7 + Nuget package version.
8 +.PARAMETER OutputDirectory
9 + Directory to place the packaged nuget file. Default to current working directory.
10 +#>
11 +
12 +[CmdletBinding(PositionalBinding=$False, DefaultParameterSetName='vm')]
13 +param (
14 + [Parameter(Mandatory = $true)][string]$InputDirectory,
15 + [Parameter(Mandatory = $true)][string]$Version,
16 + [string]$OutputDirectory = $PWD.Path
17 +)
18 +
19 +$ErrorActionPreference = "Stop"
20 +Set-StrictMode -Version Latest
21 +
22 +if (-not (Test-Path -Path $InputDirectory -PathType Container)) {
23 + throw("The path '$InputDirectory' is not an existing directory.")
24 +}
25 +
26 +$hasArch = (Test-Path "$InputDirectory\x64") -or (Test-Path "$InputDirectory\arm64")
27 +if (-not $hasArch) {
28 + throw("The input directory must contain at least one architecture subdirectory (x64, arm64).")
29 +}
30 +
31 +echo "Building test data nuget. Input: $InputDirectory. Version: $Version"
32 +
33 +Copy-Item -Path "$PSScriptRoot\Microsoft.WSL.TestData.nuspec" -Destination "$InputDirectory" -Force
34 +
35 +& "$PSScriptRoot\..\..\_deps\nuget.exe" pack "$InputDirectory\Microsoft.WSL.TestData.nuspec" -Properties "version=$Version" -OutputDirectory "$OutputDirectory"
\ No newline at end of file
tools/test/run-tests.ps1
+4 -1
@@ -9,6 +9,8 @@
9 Path to a setup script to be run prior to running the tests. Defaults to ".\test-setup.ps1".
10 .PARAMETER DistroPath
11 Path to a .tar/.tar.gz file of the distro to be imported to run the tests with. Defaults to ".\test_distro.tar.gz".
12 +.PARAMETER TestDataPath
13 + Path to test data folder. Defaults to ".\test_data".
14 .PARAMETER Package
15 Path to the wsl.msix package to install. Defaults to ".\wsl.msix".
16 .PARAMETER UnitTestsPath
@@ -28,6 +30,7 @@ param (
30 [string]$Version = 2,
31 [string]$SetupScript = ".\test-setup.ps1",
32 [string]$DistroPath = ".\test_distro.tar.gz",
33 + [string]$TestDataPath = ".\test_data",
34 [string]$Package = ".\installer.msix",
35 [string]$UnitTestsPath = ".\unit_tests",
36 [switch]$PullRequest = $false,
@@ -78,7 +81,7 @@ foreach ($arg in $TeArgs)
81 }
82 }
83
81 -$teArgList = @($TestDllPath, "/p:SetupScript=$SetupScript", "/p:Version=$Version", "/p:DistroPath=$DistroPath",
84 +$teArgList = @($TestDllPath, "/p:SetupScript=$SetupScript", "/p:Version=$Version", "/p:DistroPath=$DistroPath", "/p:TestDataPath=$TestDataPath",
85 "/p:Package=$Package", "/p:UnitTestsPath=$UnitTestsPath", "/p:PullRequest=$PullRequest", "/p:AllowUnsigned=1") + $TeArgs
86
87 if (-not $HasUserSelection)
tools/test/setup-vm-for-tests.ps1
+12 -1
@@ -29,6 +29,8 @@
29 Skip copying over the distro.
30 .PARAMETER TestDistroPath
31 Path to the distro image to import and use for testing, if needed. Auto filled if left empty.
32 +.PARAMETER TestDataPath
33 + Path to the test data folder to be copied to the VM, if needed. Auto filled if left empty.
34 #>
35
36 [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName='vm')]
@@ -46,7 +48,8 @@ param (
48 [string]$RemoteFolder = "C:\Package",
49 [string]$TaefFolder = "C:\Taef",
50 [switch]$SkipDistro,
49 - [string]$TestDistroPath
51 + [string]$TestDistroPath,
52 + [string]$TestDataPath
53 )
54
55 $ErrorActionPreference = "Stop"
@@ -98,6 +101,11 @@ if ([string]::IsNullOrEmpty($TestDistroPath)) {
101 $TestDistroPath = "$PSScriptRoot\..\..\packages\Microsoft.WSL.TestDistro.$TestDistroVersion\$Platform\test_distro.tar.xz"
102 }
103
104 +if ([string]::IsNullOrEmpty($TestDataPath)) {
105 + $TestDataVersion = (Select-Xml -Path "$PSScriptRoot\..\..\packages.config" -XPath '/packages/package[@id=''Microsoft.WSL.TestData'']/@version').Node.Value
106 + $TestDataPath = "$PSScriptRoot\..\..\packages\Microsoft.WSL.TestData.$TestDataVersion\$Platform"
107 +}
108 +
109 if ([string]::IsNullOrEmpty($ArtifactFolder)) {
110 $ArtifactFolder = "$PSScriptRoot/../.."
111 }
@@ -131,6 +139,7 @@ Invoke-Command -Session $Session -ArgumentList $RemoteFolder -ScriptBlock {
139 }
140 Copy-Item -ToSession $Session -Path "$Bin/installer.msix" -Destination $RemoteFolder -Force
141 Copy-Item -ToSession $Session -Path "$Bin/wsltests.dll" -Destination $RemoteFolder -Force
142 +Copy-Item -ToSession $Session -Path "$Bin/wslcsdk.dll" -Destination $RemoteFolder -Force
143 Copy-Item -ToSession $Session -Path "$Bin/testplugin.dll" -Destination $RemoteFolder -Force
144 Copy-Item -ToSession $Session -Path "$PSScriptRoot/test-setup.ps1" -Destination $RemoteFolder -Force
145 Copy-Item -ToSession $Session -Path "$PSScriptRoot/run-tests.ps1" -Destination $RemoteFolder -Force
@@ -140,6 +149,8 @@ if (!$SkipDistro) {
149 Copy-Item -ToSession $Session -Path $TestDistroPath -Destination "$RemoteFolder/test_distro.tar.gz" -Force
150 }
151
152 +Copy-Item -ToSession $Session -Path $TestDataPath -Destination "$RemoteFolder/test_data" -Recurse -Force
153 +
154 $taefVersion = (Select-Xml -Path "$PSScriptRoot\..\..\packages.config" -XPath '/packages/package[@id=''Microsoft.Taef'']/@version').Node.Value
155 $taefPackage = "$ArtifactFolder/packages/Microsoft.Taef.$taefVersion/build/Binaries/$Platform"
156 Copy-Item -ToSession $Session -Path "$taefPackage" -Destination $TaefFolder -Recurse -Force
tools/test/test-setup.ps1
+8 -1
@@ -59,7 +59,13 @@ if ($Package) {
59 )
60
61 $exitCode = (Start-Process -Wait "msiexec.exe" -ArgumentList $MSIArguments -NoNewWindow -PassThru).ExitCode
62 - if ($exitCode -Ne 0)
62 + # 1605 means that ProductCode was not present on the system
63 + if ($exitCode -Eq 1605)
64 + {
65 + Write-Host "MSI product $($installedMsi.ProductCode) was not found, registry HKLM:Software\Microsoft\Windows\CurrentVersion\Lxss\MSI appears to have been leaked."
66 + exit 1
67 + }
68 + elseif ($exitCode -Ne 0)
69 {
70 Write-Host "Failed to remove package: $exitCode"
71 exit 1
@@ -121,6 +127,7 @@ New-ItemProperty -Path $UserLxssRegistryPath -Name "OOBEComplete" -Value "1" -Pr
127
128 if ($DistroPath)
129 {
130 + Write-Host "Importing distro $DistroName($Version) from $DistroPath"
131 & wsl.exe --unregister "$DistroName" # Ignore non-zero return for this call
132 Run { wsl.exe --import "$DistroName" "$env:LocalAppData\lxss" "$DistroPath" --version "$Version" }
133 Run { wsl.exe --set-default "$DistroName" }
tools/test/test.bat.in
+1 -1
@@ -6,7 +6,7 @@ set "Bin=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}\${CMAKE_BUILD_TYPE}"
6 set "Tools=${CMAKE_CURRENT_LIST_DIR}\tools"
7 path %PATH%;${TAEF_SOURCE_DIR}\build\Binaries\${TARGET_PLATFORM}
8
9 -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
9 +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" -TestDataPath "${WSL_TEST_DATA_SOURCE_DIR}${TARGET_PLATFORM}" -TestDllPath "%bin%\wsltests.dll" -UnitTestsPath "${CMAKE_CURRENT_LIST_DIR}\test\linux\unit_tests" -Package "%bin%\installer.msix" %* || goto fail
10
11 exit /b 0
12