diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..dcafa74
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ - package-ecosystem: nuget
+ directory: /src
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6b25dcb
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,43 @@
+name: Build and test
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+ workflow_dispatch:
+permissions:
+ contents: read
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ test:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ dotnet-version: '10.0.x'
+ - run: dotnet restore src/WslQuery.sln -p:NuGetAuditMode=all -warnaserror
+ - run: dotnet build src/WslQuery.sln -c Release --no-restore -warnaserror
+ - run: dotnet run --project src/WslQuery.Tests -c Release --no-build
+ - name: Publish Windows Native AOT
+ if: runner.os == 'Windows'
+ run: dotnet publish src/WslQuery/WslQuery.csproj -c Release -r win-x64 -p:PublishAot=true -warnaserror -o artifacts/win-x64
+ - name: Smoke test the published executable
+ if: runner.os == 'Windows'
+ shell: pwsh
+ run: ./tests/SmokeTest.ps1 -Executable ./artifacts/win-x64/WslQuery.exe
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ if: runner.os == 'Windows'
+ with:
+ name: WslQuery-win-x64
+ path: artifacts/win-x64/
+ if-no-files-found: error
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..aacdf27
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,32 @@
+name: CodeQL
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+ schedule:
+ - cron: '17 5 * * 1'
+ workflow_dispatch:
+permissions:
+ contents: read
+jobs:
+ analyze:
+ runs-on: windows-latest
+ timeout-minutes: 20
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ dotnet-version: '10.0.x'
+ - uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
+ with:
+ languages: csharp
+ build-mode: manual
+ - run: dotnet build src/WslQuery.sln -c Release -warnaserror
+ - uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
diff --git a/README.ko-kr.md b/README.ko-kr.md
new file mode 100644
index 0000000..c486ec9
--- /dev/null
+++ b/README.ko-kr.md
@@ -0,0 +1,81 @@
+# WslQuery
+
+[English](README.md)
+
+WslQuery는 현재 Windows 사용자가 등록한 WSL 배포판의 정보를 조회해 표준 출력으로 JSON을 내보냅니다. 2026년 9월 5일 소스 개정에서는 .NET 10을 대상으로 설정하고 Windows x64 Native AOT 빌드를 구성했습니다.
+
+이번 개정에서는 한국어 문서, 기존 JSON 필드 호환성, 빌드 도구 교체, 보안 관련 코드 수정을 다룹니다. [공개 릴리스](https://github.com/wslhub/WslQuery/releases)는 현재 소스보다 이전 버전을 제공할 수 있습니다.
+
+사용 방법과 출력 규약부터 살펴보겠습니다. 빌드와 검증 절차를 설명한 뒤 보안 변경 사항과 기여 정보를 정리합니다.
+
+## JSON 조회 명령과 종료 코드
+
+Windows 터미널에서 다음 명령을 실행합니다.
+
+```console
+WslQuery.exe [--pretty] [--help]
+```
+
+- `--pretty`: 들여쓰기를 적용한 JSON 출력
+- `--help` 또는 `-h`: WSL 조회 없이 사용 방법 출력
+- 종료 코드 `0`: 조회 성공, 빈 배포판 목록 또는 도움말 출력
+- 종료 코드 `1`: 지원하지 않는 플랫폼, WSL 조회 실패 또는 일부 배포판 조회 실패
+- 종료 코드 `2`: 알 수 없는 인자
+
+프로그램은 옵션의 대소문자를 구분하지 않습니다. WSL API를 사용할 수 있으나 등록한 배포판이 없으면 빈 JSON 배열을 출력합니다. 오류 설명은 표준 오류로 내보냅니다. 일부 배포판 조회에 실패하면 JSON 결과를 유지하면서 종료 코드와 각 항목의 `hResult`, `succeed`로 실패를 알립니다. [명령 처리 코드](src/WslQuery/Program.cs)에서 세부 동작을 확인할 수 있습니다.
+
+## 기존 JSON 필드와 수정한 값
+
+출력은 Wslhub.Sdk 0.1.2의 속성 이름 16개를 유지합니다. `hResult`, `succeed`, `isDefault`, `isDefaultDistro`를 포함하며 플래그도 숫자로 출력합니다. [기존 SDK 모델](https://github.com/wslhub/wsl-sdk-dotnet/blob/78e6c9d/src/Wslhub.Sdk/DistroInfo.cs)을 기준으로 호환성을 검증합니다.
+
+다만 기본 배포판 표시와 성공 여부의 계산 오류는 수정했습니다. 등록하지 않은 배포판을 성공으로 표시하지 않으며 `defaultUid`는 Windows API가 정의한 부호 없는 32비트 범위를 사용합니다. 선택 값인 `basePath`가 없으면 JSON에 `null`을 기록합니다. JSON 직렬화기 교체로 이스케이프 표기는 달라질 수 있지만 파싱한 문자열 값은 유지합니다.
+
+## .NET 10과 Windows Native AOT 빌드
+
+빌드에 사용하는 도구를 정리하겠습니다.
+
+- 최신 서비스 업데이트를 적용한 .NET 10 SDK
+- Windows x64 환경
+- Desktop development with C++ 워크로드와 Windows SDK를 설치한 Visual Studio Build Tools
+- 실제 배포판 조회를 위한 WSL
+
+저장소 루트에서 다음 명령으로 실행 파일을 생성합니다.
+
+```powershell
+dotnet publish src/WslQuery/WslQuery.csproj -c Release -r win-x64 -p:PublishAot=true -o artifacts/win-x64
+```
+
+`src\publish.cmd`도 같은 빌드를 수행하고 종료 코드를 전달합니다. 생성한 실행 파일은 별도의 .NET 설치 없이 동작합니다. 배포 폴더의 라이선스 안내도 함께 전달합니다. macOS에서 Windows Native AOT 실행 파일을 직접 생성하는 방식은 지원하지 않습니다. Microsoft의 [Native AOT 문서](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)에서 플랫폼별 빌드 조건을 설명합니다.
+
+## 회귀 테스트와 Windows 검증 범위
+
+저장소 루트에서 빌드와 회귀 테스트를 실행합니다.
+
+```console
+dotnet build src/WslQuery.sln -c Release -warnaserror
+dotnet run --project src/WslQuery.Tests -c Release --no-build
+```
+
+테스트는 JSON 필드, 한글과 특수문자, 인자 처리, 실패 시 종료 코드, 네이티브 문자열 처리를 검증합니다. Windows 레지스트리 테스트는 `HKCU\Software\WslQuery.Tests` 아래의 고유한 테스트 키를 사용한 뒤 삭제합니다. 실제 WSL 등록 정보는 수정하지 않습니다. 다른 운영체제에서는 Windows 전용 테스트를 건너뜁니다.
+
+이어서 [GitHub Actions](.github/workflows/ci.yml)는 Linux와 Windows에서 코드를 빌드하고 Windows x64 Native AOT 실행 파일을 검증합니다. 실행기에 사용할 수 있는 WSL이 없으면 오류 처리 경로까지만 검사합니다. 실제 배포판을 등록한 WSL 1과 WSL 2의 구성 조회는 별도 통합 검증 범위로 남습니다.
+
+## 의존성 제거와 네이티브 호출 제한
+
+프로젝트는 Newtonsoft.Json, Wslhub.Sdk, 실험판 ILCompiler 참조와 실험용 NuGet 피드를 제거했습니다. .NET 10에 포함한 JSON 직렬화기와 공식 Native AOT 빌드 도구를 사용합니다. Newtonsoft.Json 제거로 [CVE-2024-21907](https://github.com/advisories/GHSA-5crp-9r3c-p9vr)의 원인이 된 패키지 의존성을 없앴습니다. 기존 앱이 외부 JSON 입력을 받아 원격 공격으로 이어지는 경로는 이번 검토에서 확인되지 않았습니다.
+
+또한 네이티브 DLL 검색은 Windows System32로 제한하고 현재 사용자의 64비트 레지스트리를 읽기 전용으로 엽니다. [WSL API 문서](https://learn.microsoft.com/en-us/windows/win32/api/wslapi/nf-wslapi-wslgetdistributionconfiguration)에 따라 환경 변수 문자열과 배열의 메모리를 해제합니다. 프로그램은 배포판 내부 명령을 실행하거나 조회 결과를 네트워크로 전송하지 않습니다.
+
+NuGet 취약성 감사와 CodeQL을 CI에 추가하고 Dependabot을 구성했습니다. Native AOT 실행 파일은 런타임 코드를 포함하므로 [.NET 10 서비스 업데이트](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core)를 적용해 다시 빌드하고 배포하면 해당 런타임 수정을 반영할 수 있습니다.
+
+## 기존 기여와 라이선스
+
+한국어 문서는 [Xeppetto의 PR #5](https://github.com/wslhub/WslQuery/pull/5)를 반영했으며 원래 작성자 커밋을 보존했습니다. WSL 조회 모델과 API 호출은 [Wslhub.Sdk](https://github.com/wslhub/wsl-sdk-dotnet)를 바탕으로 구성했고 MIT 라이선스 고지를 [THIRD-PARTY-NOTICES.txt](THIRD-PARTY-NOTICES.txt)에 포함했습니다.
+
+프로젝트의 사용 조건은 [LICENSE](LICENSE)에 명시했습니다. 톱니바퀴 아이콘은 [Icons8](https://icons8.com/icons/set/gears)에서 제공합니다.
+
+## 개정 소스의 적용 범위
+
+여기까지 정리하면 WslQuery는 현재 사용자의 WSL 정보를 JSON으로 조회하는 도구로 동작합니다. 이번 소스 개정은 오래된 의존성과 조회 오류를 수정하며 공개 릴리스 갱신은 별도의 배포 절차로 진행합니다.
+
+기존 스크립트는 JSON 필드와 종료 코드 변경을 기준으로 검증할 수 있습니다. 배포 담당자는 Windows Native AOT 빌드 결과와 실제 WSL 배포판 조회 결과를 함께 확인하면 적용 범위를 판단할 수 있습니다. 런타임 서비스 업데이트를 반영하는 작업은 이후 유지보수 과정에서도 이어집니다.
diff --git a/README.md b/README.md
index 66f4c9c..c19ef92 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,74 @@
# WslQuery
+[한국어](README.ko-kr.md)
+

-[Download latest version](https://github.com/rkttu/WslQuery/releases/latest/download/WslQuery.exe)
+WslQuery reads the current Windows user's registered WSL distributions and writes their configuration to standard output as JSON. This source targets .NET 10 and publishes a standalone Windows x64 Native AOT executable.
+
+[Published releases](https://github.com/wslhub/WslQuery/releases) may predate these source changes. The screenshot shows an earlier release.
+
+## Usage and exit codes
+
+Run the executable from a Windows terminal:
+
+```console
+WslQuery.exe [--pretty] [--help]
+```
+
+- `--pretty`: indent JSON output. Option matching is case-insensitive.
+- `--help` or `-h`: display usage without accessing WSL.
+- Exit `0`: all queried distributions succeeded, an empty list, or help.
+- Exit `1`: unsupported platform, WSL/query failure, or at least one failed distribution.
+- Exit `2`: unknown argument.
-This program is a utility that reads the internal information of Windows Subsystem for Linux from the system and outputs the data to a standard output device in JSON format.
+Successful queries emit a JSON array. An installed WSL API with no distribution registrations produces `[]`. Diagnostics go to stderr. Partial query failures preserve the JSON results and report failure through the exit code and each item's `hResult` and `succeed` fields.
-## Usage
+## JSON compatibility
-The usage of the program is simple.
+The output keeps all 16 property names from Wslhub.Sdk 0.1.2, including `hResult`, `succeed`, `isDefault` and `isDefaultDistro`. Flags remain JSON numbers. System.Text.Json source generation removes reflection-based serialization and the Newtonsoft.Json dependency.
-`WslQuery.exe [--pretty]`
+The modernization corrects default-distribution flags and prevents an unregistered distribution from reporting success. `defaultUid` uses the Windows API's unsigned 32-bit range. Missing optional `basePath` values appear as `null`. JSON escaping may differ from older releases; consumers can parse the values instead of comparing raw text. See the [original SDK contract](https://github.com/wslhub/wsl-sdk-dotnet/blob/78e6c9d/src/Wslhub.Sdk/DistroInfo.cs) and [WSL configuration API](https://learn.microsoft.com/en-us/windows/win32/api/wslapi/nf-wslapi-wslgetdistributionconfiguration).
-* `--pretty`: This option prints JSON data in a human-readable form.
+## Build and Native AOT publishing
-## Build Environment
+Build prerequisites:
-- .NET 5
-- Native AOT Compiler
+- A supported Windows x64 installation with WSL for live queries
+- The latest serviced .NET 10 SDK; `global.json` accepts stable .NET 10 feature bands
+- Visual Studio Build Tools with Desktop development with C++ and a Windows SDK for Native AOT
-## Credits
+Publish from the repository root on Windows:
+```powershell
+dotnet publish src/WslQuery/WslQuery.csproj -c Release -r win-x64 -p:PublishAot=true -o artifacts/win-x64
```
-Gears Icon
-icons8.com (https://icons8.com/icons/set/gears)
+
+`src\publish.cmd` runs the same publish command and propagates its exit code. The published `WslQuery.exe` runs without a separate .NET installation. Distribute the included license notices alongside it. Cross-OS Native AOT compilation is not supported; see the [Native AOT prerequisites](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/).
+
+## Regression and Windows validation
+
+Run the dependency-free regression executable from the repository root:
+
+```console
+dotnet build src/WslQuery.sln -c Release -warnaserror
+dotnet run --project src/WslQuery.Tests -c Release --no-build
```
+
+The tests cover the JSON contract, argument and error handling, UTF-8 native buffers and Windows registry enumeration. Registry fixtures use a unique key under `HKCU\Software\WslQuery.Tests`, remove it afterwards and never modify WSL registrations. Windows-only cases are skipped on other operating systems.
+
+GitHub Actions builds on Linux and Windows, publishes Windows x64 Native AOT, and runs `tests/SmokeTest.ps1` against the executable. A runner without usable WSL only verifies the controlled failure path. Successful configuration queries against populated WSL 1 and WSL 2 distributions remain a separate integration check. See the [test workflow](.github/workflows/ci.yml).
+
+## Dependency and native-call security
+
+The application has no explicit runtime NuGet package dependencies. It uses the .NET 10 shared libraries and official SDK Native AOT tooling. The old experimental feed and ILCompiler alpha package are removed. NuGet auditing runs during CI restores; CodeQL and Dependabot configuration live under `.github`.
+
+Native calls use source-generated bindings and restrict DLL lookup to Windows System32. Registry access is read-only and uses the current user's 64-bit view. Environment strings and their native array are released with `CoTaskMemFree`, as required by the [WSL API contract](https://learn.microsoft.com/en-us/windows/win32/api/wslapi/nf-wslapi-wslgetdistributionconfiguration). WslQuery does not launch a distribution command or send query output to a network service.
+
+Removing Newtonsoft.Json addresses the dependency behind [CVE-2024-21907](https://github.com/advisories/GHSA-5crp-9r3c-p9vr). The old application only serialized its WSL model; this review did not establish a remotely exploitable JSON-input path. Native AOT executables bundle runtime code, so rebuilding and redistributing with current [.NET 10 servicing updates](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core) also matters.
+
+## Credits and license
+
+The Korean README incorporates [Xeppetto's contribution in PR #5](https://github.com/wslhub/WslQuery/pull/5). Its original author commit is retained.
+
+WSL query models and bindings derive from [Wslhub.Sdk](https://github.com/wslhub/wsl-sdk-dotnet). Its MIT notice is included in [THIRD-PARTY-NOTICES.txt](THIRD-PARTY-NOTICES.txt). WslQuery is covered by [LICENSE](LICENSE). The gears icon comes from [Icons8](https://icons8.com/icons/set/gears).
diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt
new file mode 100644
index 0000000..08eb89f
--- /dev/null
+++ b/THIRD-PARTY-NOTICES.txt
@@ -0,0 +1,29 @@
+WSL query models and API bindings are adapted from Wslhub.Sdk 0.1.2.
+Source: https://github.com/wslhub/wsl-sdk-dotnet
+
+MIT License
+
+Copyright (c) 2020 WSL Korea User Group (WSLHUB)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---
+
+Gears Icon
+icons8.com (https://icons8.com/icons/set/gears)
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..d46d21e
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "10.0.100",
+ "rollForward": "latestFeature",
+ "allowPrerelease": false
+ }
+}
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..765346e
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/.vscode/launch.json b/src/.vscode/launch.json
index 9401444..30215cf 100644
--- a/src/.vscode/launch.json
+++ b/src/.vscode/launch.json
@@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
- "program": "${workspaceFolder}/WslQuery/bin/Debug/netcoreapp3.1/WslQuery.dll",
+ "program": "${workspaceFolder}/WslQuery/bin/Debug/net10.0/WslQuery.dll",
"args": [],
"cwd": "${workspaceFolder}/WslQuery",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
diff --git a/src/.vscode/tasks.json b/src/.vscode/tasks.json
index be73374..32c21b3 100644
--- a/src/.vscode/tasks.json
+++ b/src/.vscode/tasks.json
@@ -20,8 +20,11 @@
"args": [
"publish",
"${workspaceFolder}/WslQuery/WslQuery.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary"
+ "-c",
+ "Release",
+ "-r",
+ "win-x64",
+ "-p:PublishAot=true"
],
"problemMatcher": "$msCompile"
},
@@ -31,12 +34,11 @@
"type": "process",
"args": [
"watch",
- "run",
+ "--project",
"${workspaceFolder}/WslQuery/WslQuery.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary"
+ "run"
],
"problemMatcher": "$msCompile"
}
]
-}
\ No newline at end of file
+}
diff --git a/src/WslQuery.Tests/Program.cs b/src/WslQuery.Tests/Program.cs
new file mode 100644
index 0000000..90c3a71
--- /dev/null
+++ b/src/WslQuery.Tests/Program.cs
@@ -0,0 +1,240 @@
+using Microsoft.Win32;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using WslQuery;
+
+var tests = new List<(string Name, Action Run)>
+{
+ ("Legacy JSON field names and numeric values", JsonContract),
+ ("Pretty JSON preserves values and nulls", PrettyJson),
+ ("Empty results remain a JSON array", () => Equal("[]", QueryJson.Serialize([], false))),
+ ("Help skips the native query", Help),
+ ("Unknown arguments fail without echoing terminal controls", UnknownArgument),
+ ("Case-insensitive pretty option", PrettyOption),
+ ("Query failure uses stderr and a nonzero exit code", QueryFailure),
+ ("Partial query failure preserves JSON and returns failure", PartialFailure),
+ ("Unregistered distribution does not report success", Unregistered),
+ ("Native UTF-8 environment array", NativeStrings),
+ ("Empty native array", () => Equal(0, NativeEnvironment.ReadAndFree(0, 0).Count)),
+ ("Invalid native array fails safely", InvalidNativeArray),
+ ("COM security initialization success", () => ComSecurity.ThrowIfInitializationFailed(0)),
+ ("Previously initialized COM security is accepted", () => ComSecurity.ThrowIfInitializationFailed(unchecked((int)0x80010119))),
+ ("Other COM security failures are preserved", ComSecurityFailures)
+};
+if (OperatingSystem.IsWindows())
+{
+ tests.Add(("Registry enumeration with isolated Windows fixtures", RegistryEnumeration));
+ tests.Add(("Repeated native COM security initialization", RepeatedComSecurityInitialization));
+}
+else
+ Console.WriteLine("SKIP: Registry fixtures and native COM initialization require Windows.");
+
+var failed = 0;
+foreach (var (name, run) in tests)
+{
+ try { run(); Console.WriteLine($"PASS: {name}"); }
+ catch (Exception ex) { failed++; Console.Error.WriteLine($"FAIL: {name}: {ex}"); }
+}
+Console.WriteLine($"{tests.Count - failed}/{tests.Count} tests passed.");
+return failed == 0 ? 0 : 1;
+
+static void Equal(T expected, T actual)
+{
+ if (!EqualityComparer.Default.Equals(expected, actual))
+ throw new Exception($"Expected {expected}, got {actual}.");
+}
+
+static DistroInfo Sample() => new()
+{
+ DistroId = Guid.Parse("d596e687-441e-4478-a793-b51d46a3b847"),
+ DistroName = "Ubuntu-한국어\"\\\n",
+ BasePath = @"C:\Users\Example\Distro",
+ DefaultEnvironmentVariables = ["LANG=ko_KR.UTF-8", "VALUE=<>&\""],
+ KernelCommandLine = ["quiet"],
+ IsRegistered = true,
+ IsDefault = true,
+ WslVersion = 2,
+ DefaultUid = uint.MaxValue,
+ DistroFlags = 7
+};
+
+static void JsonContract()
+{
+ var json = QueryJson.Serialize([Sample()], false);
+ Equal(false, json.Contains('\n'));
+ using var document = JsonDocument.Parse(json);
+ var value = document.RootElement[0];
+ // Golden field list comes from Wslhub.Sdk 0.1.2 and Newtonsoft's camel-case resolver.
+ var expected = "appendNtPath,basePath,defaultEnvironmentVariables,defaultUid,distroFlags,distroId,distroName,enableDriveMounting,enableInterop,hResult,isDefault,isDefaultDistro,isRegistered,kernelCommandLine,succeed,wslVersion";
+ Equal(expected, string.Join(",", value.EnumerateObject().Select(p => p.Name).Order(StringComparer.Ordinal)));
+ Equal(uint.MaxValue, value.GetProperty("defaultUid").GetUInt32());
+ Equal(7, value.GetProperty("distroFlags").GetInt32());
+ Equal(0, value.GetProperty("hResult").GetInt32());
+ Equal(Sample().DistroName, value.GetProperty("distroName").GetString());
+ foreach (var name in new[] { "isDefault", "isDefaultDistro", "isRegistered", "succeed", "enableInterop", "enableDriveMounting", "appendNtPath" })
+ Equal(true, value.GetProperty(name).GetBoolean());
+}
+
+static void PrettyJson()
+{
+ var sample = Sample();
+ sample.BasePath = null;
+ var compact = QueryJson.Serialize([sample], false);
+ var pretty = QueryJson.Serialize([sample], true);
+ Equal(true, pretty.Contains('\n'));
+ using var first = JsonDocument.Parse(compact);
+ using var second = JsonDocument.Parse(pretty);
+ Equal(true, JsonElement.DeepEquals(first.RootElement, second.RootElement));
+ Equal(JsonValueKind.Null, second.RootElement[0].GetProperty("basePath").ValueKind);
+}
+
+static void Help()
+{
+ using var output = new StringWriter();
+ using var error = new StringWriter();
+ Equal(0, CommandLine.Run(["--help"], output, error, () => throw new Exception("Query must not run")));
+ Equal(CommandLine.Usage + Environment.NewLine, output.ToString());
+ Equal("", error.ToString());
+}
+
+static void UnknownArgument()
+{
+ using var output = new StringWriter();
+ using var error = new StringWriter();
+ Equal(2, CommandLine.Run(["--help", "\u001b[2J"], output, error, () => throw new Exception("Query must not run")));
+ Equal("", output.ToString());
+ Equal(false, error.ToString().Contains('\u001b'));
+}
+
+static void PrettyOption()
+{
+ using var output = new StringWriter();
+ using var error = new StringWriter();
+ Equal(0, CommandLine.Run(["--PRETTY"], output, error, () => [Sample()]));
+ Equal(true, output.ToString().Contains(" {"));
+ Equal("", error.ToString());
+}
+
+static void QueryFailure()
+{
+ using var output = new StringWriter();
+ using var error = new StringWriter();
+ Equal(1, CommandLine.Run([], output, error, () => throw new PlatformNotSupportedException("Test failure")));
+ Equal("", output.ToString());
+ Equal("WslQuery: Test failure" + Environment.NewLine, error.ToString());
+}
+
+static void PartialFailure()
+{
+ var failure = Sample();
+ failure.HResult = unchecked((int)0x80070005);
+ using var output = new StringWriter();
+ using var error = new StringWriter();
+ Equal(1, CommandLine.Run([], output, error, () => [Sample(), failure]));
+ using var json = JsonDocument.Parse(output.ToString());
+ Equal(2, json.RootElement.GetArrayLength());
+ Equal(false, json.RootElement[1].GetProperty("succeed").GetBoolean());
+ Equal(failure.HResult, json.RootElement[1].GetProperty("hResult").GetInt32());
+ Equal(true, error.ToString().Contains("hResult"));
+}
+
+static void Unregistered()
+{
+ var sample = Sample();
+ sample.IsRegistered = false;
+ Equal(false, sample.Succeed);
+}
+
+static void NativeStrings()
+{
+ var array = Marshal.AllocCoTaskMem(2 * IntPtr.Size);
+ Marshal.WriteIntPtr(array, 0, Marshal.StringToCoTaskMemUTF8("LANG=한국어"));
+ Marshal.WriteIntPtr(array, IntPtr.Size, Marshal.StringToCoTaskMemUTF8("EMPTY="));
+ // ReadAndFree owns both strings and the array from this point onwards.
+ var result = NativeEnvironment.ReadAndFree(array, 2);
+ Equal(2, result.Count);
+ Equal("LANG=한국어", result[0]);
+ Equal("EMPTY=", result[1]);
+}
+
+static void InvalidNativeArray()
+{
+ try { NativeEnvironment.ReadAndFree(0, 1); }
+ catch (InvalidDataException) { return; }
+ throw new Exception("Expected InvalidDataException");
+}
+
+static void ComSecurityFailures()
+{
+ foreach (var result in new[] { unchecked((int)0x80004005), unchecked((int)0x80070005), unchecked((int)0x8001011A) })
+ {
+ try { ComSecurity.ThrowIfInitializationFailed(result); }
+ catch (Exception ex)
+ {
+ Equal(result, ex.HResult);
+ continue;
+ }
+ throw new Exception($"Expected initialization failure for HRESULT 0x{result:X8}.");
+ }
+}
+
+static void RepeatedComSecurityInitialization()
+{
+ if (!OperatingSystem.IsWindows())
+ throw new PlatformNotSupportedException();
+
+ Marshal.ThrowExceptionForHR(NativeMethods.CoInitializeEx(0, 0));
+ try
+ {
+ ComSecurity.ThrowIfInitializationFailed(NativeMethods.CoInitializeSecurity(0, -1, 0, 0, 0, 3, 0, 0x20, 0));
+ var repeated = NativeMethods.CoInitializeSecurity(0, -1, 0, 0, 0, 3, 0, 0x20, 0);
+ Equal(unchecked((int)0x80010119), repeated);
+ ComSecurity.ThrowIfInitializationFailed(repeated);
+ }
+ finally
+ {
+ NativeMethods.CoUninitialize();
+ }
+}
+
+static void RegistryEnumeration()
+{
+ if (!OperatingSystem.IsWindows())
+ throw new PlatformNotSupportedException();
+ Equal(0, WindowsDistroQuery.ReadRegistry(null).Count);
+ var path = @"Software\WslQuery.Tests\" + Guid.NewGuid().ToString("N");
+ using var currentUser = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64);
+ try
+ {
+ using var root = currentUser.CreateSubKey(path);
+ Equal(0, WindowsDistroQuery.ReadRegistry(root).Count);
+ var firstId = Guid.NewGuid();
+ var secondId = Guid.NewGuid();
+ root.SetValue("DefaultDistribution", firstId.ToString("B"));
+ using (var first = root.CreateSubKey(firstId.ToString("B")))
+ {
+ first.SetValue("DistributionName", "Ubuntu");
+ first.SetValue("BasePath", @"C:\Fixtures\Ubuntu");
+ first.SetValue("KernelCommandLine", "quiet\t debug ");
+ }
+ using (var second = root.CreateSubKey(secondId.ToString("B")))
+ second.SetValue("DistributionName", "Debian"); // Missing optional values are valid.
+ using (root.CreateSubKey("not-a-distribution")) { }
+ using (var invalid = root.CreateSubKey(Guid.NewGuid().ToString("B")))
+ invalid.SetValue("DistributionName", " ");
+ var results = WindowsDistroQuery.ReadRegistry(root);
+ Equal(2, results.Count);
+ Equal(1, results.Count(distro => distro.IsDefault && distro.IsDefaultDistro));
+ Equal("quiet,debug", string.Join(",", results.Single(distro => distro.DistroId == firstId).KernelCommandLine));
+ Equal(null, results.Single(distro => distro.DistroId == secondId).BasePath);
+ Equal(true, results.All(distro => !distro.Succeed));
+ using (var emptyId = root.CreateSubKey(Guid.Empty.ToString("B")))
+ emptyId.SetValue("DistributionName", "EmptyGuidFixture");
+ root.SetValue("DefaultDistribution", "malformed");
+ Equal(0, WindowsDistroQuery.ReadRegistry(root).Count(distro => distro.IsDefault));
+ }
+ finally
+ {
+ currentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false);
+ }
+}
diff --git a/src/WslQuery.Tests/WslQuery.Tests.csproj b/src/WslQuery.Tests/WslQuery.Tests.csproj
new file mode 100644
index 0000000..a0037f4
--- /dev/null
+++ b/src/WslQuery.Tests/WslQuery.Tests.csproj
@@ -0,0 +1,11 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
diff --git a/src/WslQuery.sln b/src/WslQuery.sln
index d68a12e..18e7cd5 100644
--- a/src/WslQuery.sln
+++ b/src/WslQuery.sln
@@ -1,25 +1,30 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30717.126
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WslQuery", "WslQuery\WslQuery.csproj", "{D067CAE0-7642-459F-9E87-E6FACBEE261C}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|x64 = Debug|x64
- Release|x64 = Release|x64
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {D067CAE0-7642-459F-9E87-E6FACBEE261C}.Debug|x64.ActiveCfg = Debug|x64
- {D067CAE0-7642-459F-9E87-E6FACBEE261C}.Debug|x64.Build.0 = Debug|x64
- {D067CAE0-7642-459F-9E87-E6FACBEE261C}.Release|x64.ActiveCfg = Release|x64
- {D067CAE0-7642-459F-9E87-E6FACBEE261C}.Release|x64.Build.0 = Release|x64
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {577C8161-C515-45FD-8A26-8A26A3D1D97C}
- EndGlobalSection
-EndGlobal
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30717.126
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WslQuery", "WslQuery\WslQuery.csproj", "{D067CAE0-7642-459F-9E87-E6FACBEE261C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WslQuery.Tests", "WslQuery.Tests\WslQuery.Tests.csproj", "{9125CADC-DAC0-4945-BAC7-2A4A2AD03491}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D067CAE0-7642-459F-9E87-E6FACBEE261C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D067CAE0-7642-459F-9E87-E6FACBEE261C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D067CAE0-7642-459F-9E87-E6FACBEE261C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D067CAE0-7642-459F-9E87-E6FACBEE261C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9125CADC-DAC0-4945-BAC7-2A4A2AD03491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9125CADC-DAC0-4945-BAC7-2A4A2AD03491}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9125CADC-DAC0-4945-BAC7-2A4A2AD03491}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9125CADC-DAC0-4945-BAC7-2A4A2AD03491}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {577C8161-C515-45FD-8A26-8A26A3D1D97C}
+ EndGlobalSection
+EndGlobal
diff --git a/src/WslQuery/ComSecurity.cs b/src/WslQuery/ComSecurity.cs
new file mode 100644
index 0000000..3a04d8f
--- /dev/null
+++ b/src/WslQuery/ComSecurity.cs
@@ -0,0 +1,16 @@
+using System.Runtime.InteropServices;
+
+namespace WslQuery;
+
+internal static class ComSecurity
+{
+ internal const int AlreadyInitialized = unchecked((int)0x80010119); // RPC_E_TOO_LATE
+
+ internal static void ThrowIfInitializationFailed(int result)
+ {
+ // COM security is process-wide and cannot be replaced after initialization.
+ // Continue with the existing policy; any subsequent WSL failure is still reported.
+ if (result != AlreadyInitialized)
+ Marshal.ThrowExceptionForHR(result);
+ }
+}
diff --git a/src/WslQuery/DistroInfo.cs b/src/WslQuery/DistroInfo.cs
new file mode 100644
index 0000000..5de30ab
--- /dev/null
+++ b/src/WslQuery/DistroInfo.cs
@@ -0,0 +1,22 @@
+namespace WslQuery;
+
+// Retains the Wslhub.Sdk 0.1.2 JSON contract, including numeric flags and HRESULTs.
+internal sealed class DistroInfo
+{
+ public bool Succeed => IsRegistered && HResult == 0;
+ public List DefaultEnvironmentVariables { get; set; } = [];
+ public uint DefaultUid { get; set; }
+ public bool EnableInterop => (DistroFlags & 1) != 0;
+ public bool EnableDriveMounting => (DistroFlags & 4) != 0;
+ public bool AppendNtPath => (DistroFlags & 2) != 0;
+ public uint DistroFlags { get; set; }
+ public bool IsRegistered { get; set; }
+ public bool IsDefaultDistro => IsDefault;
+ public int HResult { get; set; }
+ public uint WslVersion { get; set; }
+ public Guid DistroId { get; set; }
+ public string DistroName { get; set; } = "";
+ public List KernelCommandLine { get; set; } = [];
+ public string? BasePath { get; set; }
+ public bool IsDefault { get; set; }
+}
diff --git a/src/WslQuery/NativeMethods.cs b/src/WslQuery/NativeMethods.cs
new file mode 100644
index 0000000..d9e68f3
--- /dev/null
+++ b/src/WslQuery/NativeMethods.cs
@@ -0,0 +1,33 @@
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace WslQuery;
+
+[SupportedOSPlatform("windows")]
+internal static partial class NativeMethods
+{
+ [LibraryImport("ole32.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ internal static partial int CoInitializeEx(nint reserved, uint coInit);
+
+ [LibraryImport("ole32.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ internal static partial void CoUninitialize();
+
+ [LibraryImport("ole32.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ internal static partial int CoInitializeSecurity(nint securityDescriptor, int authServiceCount,
+ nint authServices, nint reserved1, uint authenticationLevel, uint impersonationLevel,
+ nint authList, uint capabilities, nint reserved3);
+
+ [LibraryImport("wslapi.dll", StringMarshalling = StringMarshalling.Utf16)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool WslIsDistributionRegistered(string distributionName);
+
+ [LibraryImport("wslapi.dll", StringMarshalling = StringMarshalling.Utf16)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ internal static partial int WslGetDistributionConfiguration(string distributionName,
+ out uint distributionVersion, out uint defaultUid, out uint flags,
+ out nint environmentVariables, out uint environmentVariableCount);
+}
diff --git a/src/WslQuery/Program.cs b/src/WslQuery/Program.cs
index 727b358..cac80d8 100644
--- a/src/WslQuery/Program.cs
+++ b/src/WslQuery/Program.cs
@@ -1,26 +1,70 @@
-using Newtonsoft.Json;
-using Newtonsoft.Json.Serialization;
-using System;
-using System.Linq;
-using Wslhub.Sdk;
-
-namespace WslQuery
-{
- internal static class Program
- {
- [MTAThread]
- private static void Main(string[] args)
- {
- Wsl.InitializeSecurityModel();
-
- var hasPretty = string.Equals("--pretty", args?.FirstOrDefault(), StringComparison.OrdinalIgnoreCase);
- var queryResult = Wsl.GetDistroQueryResult();
-
- Console.Out.WriteLine(JsonConvert.SerializeObject(queryResult, new JsonSerializerSettings()
- {
- ContractResolver = new CamelCasePropertyNamesContractResolver(),
- Formatting = hasPretty ? Formatting.Indented : Formatting.None,
- }));
- }
- }
-}
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace WslQuery;
+
+internal static class Program
+{
+ private static int Main(string[] args)
+ {
+ Console.OutputEncoding = new UTF8Encoding(false);
+ return CommandLine.Run(args, Console.Out, Console.Error, Query);
+ }
+
+ private static List Query()
+ {
+ if (!OperatingSystem.IsWindows() || !Environment.Is64BitProcess)
+ throw new PlatformNotSupportedException("WslQuery requires 64-bit Windows with WSL installed.");
+
+ return WindowsDistroQuery.Query();
+ }
+}
+
+internal static class CommandLine
+{
+ internal const string Usage = "Usage: WslQuery.exe [--pretty] [--help]";
+
+ internal static int Run(string[] args, TextWriter output, TextWriter error, Func> query)
+ {
+ var pretty = false;
+ var help = false;
+ foreach (var arg in args)
+ {
+ if (string.Equals(arg, "--pretty", StringComparison.OrdinalIgnoreCase))
+ pretty = true;
+ else if (string.Equals(arg, "--help", StringComparison.OrdinalIgnoreCase) || arg == "-h")
+ help = true;
+ else
+ {
+ error.WriteLine("WslQuery: unknown argument.");
+ error.WriteLine(Usage);
+ return 2;
+ }
+ }
+
+ if (help)
+ {
+ output.WriteLine(Usage);
+ return 0;
+ }
+
+ try
+ {
+ var results = query();
+ output.WriteLine(QueryJson.Serialize(results, pretty));
+ if (results.Any(distro => !distro.Succeed))
+ {
+ error.WriteLine("WslQuery: some distributions could not be queried; inspect hResult in the JSON output.");
+ return 1;
+ }
+ return 0;
+ }
+ catch (Exception ex) when (ex is PlatformNotSupportedException or DllNotFoundException
+ or EntryPointNotFoundException or BadImageFormatException or ExternalException
+ or IOException or UnauthorizedAccessException or System.Security.SecurityException)
+ {
+ error.WriteLine($"WslQuery: {ex.Message}");
+ return 1;
+ }
+ }
+}
diff --git a/src/WslQuery/QueryJson.cs b/src/WslQuery/QueryJson.cs
new file mode 100644
index 0000000..f89ec40
--- /dev/null
+++ b/src/WslQuery/QueryJson.cs
@@ -0,0 +1,20 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace WslQuery;
+
+internal static class QueryJson
+{
+ private static readonly QueryJsonContext PrettyContext = new(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = true
+ });
+
+ internal static string Serialize(List results, bool pretty) =>
+ JsonSerializer.Serialize(results, (pretty ? PrettyContext : QueryJsonContext.Default).ListDistroInfo);
+}
+
+[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+[JsonSerializable(typeof(List))]
+internal partial class QueryJsonContext : JsonSerializerContext;
diff --git a/src/WslQuery/WindowsDistroQuery.cs b/src/WslQuery/WindowsDistroQuery.cs
new file mode 100644
index 0000000..fe4a3a7
--- /dev/null
+++ b/src/WslQuery/WindowsDistroQuery.cs
@@ -0,0 +1,117 @@
+using Microsoft.Win32;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace WslQuery;
+
+[SupportedOSPlatform("windows")]
+internal static class WindowsDistroQuery
+{
+ private const string LxssPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss";
+ private const int DistributionNotFound = unchecked((int)0x80070002);
+
+ internal static List Query()
+ {
+ // Resolve the DLL before querying the registry so missing WSL is an error, not an empty list.
+ var library = NativeLibrary.Load("wslapi.dll", typeof(NativeMethods).Assembly, DllImportSearchPath.System32);
+ NativeLibrary.Free(library);
+
+ using var currentUser = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64);
+ using var lxss = currentUser.OpenSubKey(LxssPath, writable: false);
+ var results = ReadRegistry(lxss);
+ if (results.Count == 0)
+ return results;
+
+ Marshal.ThrowExceptionForHR(NativeMethods.CoInitializeEx(0, 0)); // COINIT_MULTITHREADED
+ try
+ {
+ // Request WSL client impersonation and static cloaking if security is not already initialized.
+ ComSecurity.ThrowIfInitializationFailed(NativeMethods.CoInitializeSecurity(0, -1, 0, 0, 0, 3, 0, 0x20, 0));
+ foreach (var distro in results)
+ PopulateConfiguration(distro);
+ }
+ finally
+ {
+ NativeMethods.CoUninitialize();
+ }
+ return results;
+ }
+
+ internal static List ReadRegistry(RegistryKey? lxss)
+ {
+ var results = new List();
+ if (lxss is null)
+ return results;
+
+ var hasDefault = Guid.TryParse(lxss.GetValue("DefaultDistribution") as string, out var defaultId);
+ foreach (var keyName in lxss.GetSubKeyNames())
+ {
+ if (!Guid.TryParse(keyName, out var distroId))
+ continue;
+
+ using var key = lxss.OpenSubKey(keyName, writable: false);
+ if (key?.GetValue("DistributionName") is not string name || string.IsNullOrWhiteSpace(name))
+ continue;
+
+ results.Add(new DistroInfo
+ {
+ DistroId = distroId,
+ DistroName = name,
+ BasePath = key.GetValue("BasePath") as string,
+ KernelCommandLine = (key.GetValue("KernelCommandLine") as string ?? "")
+ .Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries).ToList(),
+ IsDefault = hasDefault && distroId == defaultId,
+ HResult = DistributionNotFound
+ });
+ }
+ return results;
+ }
+
+ private static void PopulateConfiguration(DistroInfo distro)
+ {
+ distro.IsRegistered = NativeMethods.WslIsDistributionRegistered(distro.DistroName);
+ if (!distro.IsRegistered)
+ return;
+
+ distro.HResult = NativeMethods.WslGetDistributionConfiguration(distro.DistroName,
+ out var version, out var uid, out var flags, out var variables, out var count);
+ // The API only guarantees ownership of its output buffers after a successful call.
+ if (distro.HResult != 0)
+ return;
+
+ distro.DefaultEnvironmentVariables = NativeEnvironment.ReadAndFree(variables, count);
+ distro.WslVersion = version;
+ distro.DefaultUid = uid;
+ distro.DistroFlags = flags;
+ }
+}
+
+internal static class NativeEnvironment
+{
+ internal static List ReadAndFree(nint variables, uint count)
+ {
+ if (variables == 0)
+ {
+ if (count != 0)
+ throw new InvalidDataException("WSL returned an invalid environment variable array.");
+ return [];
+ }
+
+ try
+ {
+ var results = new List();
+ for (uint index = 0; index < count; index++)
+ {
+ var pointer = Marshal.ReadIntPtr(variables, checked((int)index * IntPtr.Size));
+ results.Add(Marshal.PtrToStringUTF8(pointer) ?? "");
+ }
+ return results;
+ }
+ finally
+ {
+ for (uint index = 0; index < count; index++)
+ Marshal.FreeCoTaskMem(Marshal.ReadIntPtr(variables, checked((int)index * IntPtr.Size)));
+ Marshal.FreeCoTaskMem(variables);
+ }
+ }
+}
diff --git a/src/WslQuery/WslQuery.csproj b/src/WslQuery/WslQuery.csproj
index 418674c..9a78bb8 100644
--- a/src/WslQuery/WslQuery.csproj
+++ b/src/WslQuery/WslQuery.csproj
@@ -1,64 +1,28 @@
-
-
-
- Exe
- net5.0
- Program
- rkttu.com
- This application lookup WSL installation status and return all relative informations into JSON.
- (c) 2020 rkttu.com, All rights reserved.
- https://github.com/wslhub/WslQuery
- https://github.com/wslhub/WslQuery
- git
- WslQuery
- 0.5.0.0
- 0.5.0.0
- LICENSE.txt
- App.ico
- WslQuery.Program
- true
- false
- App.ico
- x64
-
-
-
- true
-
-
-
- true
-
-
-
- true
- false
- false
- false
- Size
- true
- app.manifest
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
-
-
-
-
-
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ true
+ true
+ false
+ true
+ Size
+ rkttu.com
+ Queries WSL distributions and writes their configuration as JSON.
+ (c) 2020 rkttu.com, All rights reserved.
+ https://github.com/wslhub/WslQuery
+ https://github.com/wslhub/WslQuery
+ git
+ 0.5.0.0
+ 0.5.0.0
+ App.ico
+ app.manifest
+
+
+
+
+
+
+
diff --git a/src/WslQuery/nuget.config b/src/WslQuery/nuget.config
deleted file mode 100644
index 3c01b30..0000000
--- a/src/WslQuery/nuget.config
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/publish.cmd b/src/publish.cmd
index 7f32c97..6613439 100644
--- a/src/publish.cmd
+++ b/src/publish.cmd
@@ -1,10 +1,7 @@
-@echo off
-pushd "%~dp0"
-
-pushd WslQuery
-dotnet publish -c Release -r win-x64
-popd
-
-:exit
-popd
-@echo on
\ No newline at end of file
+@echo off
+setlocal
+pushd "%~dp0.."
+dotnet publish src\WslQuery\WslQuery.csproj -c Release -r win-x64 -p:PublishAot=true -o artifacts\win-x64
+set "publishResult=%errorlevel%"
+popd
+exit /b %publishResult%
diff --git a/tests/SmokeTest.ps1 b/tests/SmokeTest.ps1
new file mode 100644
index 0000000..9f41aad
--- /dev/null
+++ b/tests/SmokeTest.ps1
@@ -0,0 +1,39 @@
+param([Parameter(Mandatory)][string]$Executable)
+$ErrorActionPreference = 'Stop'
+$Executable = (Resolve-Path $Executable).Path
+
+function Invoke-Query([string[]]$Arguments) {
+ $start = [System.Diagnostics.ProcessStartInfo]::new($Executable)
+ $start.UseShellExecute = $false
+ $start.RedirectStandardOutput = $true
+ $start.RedirectStandardError = $true
+ foreach ($argument in $Arguments) { $start.ArgumentList.Add($argument) }
+ $process = [System.Diagnostics.Process]::Start($start)
+ $stdout = $process.StandardOutput.ReadToEndAsync()
+ $stderr = $process.StandardError.ReadToEndAsync()
+ if (-not $process.WaitForExit(30000)) {
+ $process.Kill($true)
+ throw 'WslQuery timed out.'
+ }
+ $result = @{ Code = $process.ExitCode; Out = $stdout.GetAwaiter().GetResult(); Err = $stderr.GetAwaiter().GetResult() }
+ $process.Dispose()
+ return $result
+}
+
+$help = Invoke-Query -Arguments @('--help')
+if ($help.Code -ne 0 -or $help.Out -notmatch '^Usage:' -or $help.Err) { throw 'Help smoke test failed.' }
+$invalid = Invoke-Query -Arguments @('--unknown')
+if ($invalid.Code -ne 2 -or $invalid.Out -or -not $invalid.Err) { throw 'Argument smoke test failed.' }
+
+$query = Invoke-Query -Arguments @('--pretty')
+if ($query.Code -eq 0) {
+ if ($query.Err -or -not $query.Out.TrimStart().StartsWith('[')) { throw 'Invalid successful query output.' }
+ $null = ConvertFrom-Json -InputObject $query.Out
+ Write-Output 'Native AOT query succeeded. The runner may have no registered distributions.'
+} elseif ($query.Code -eq 1 -and $query.Err.StartsWith('WslQuery:')) {
+ if ($query.Out) { $null = ConvertFrom-Json -InputObject $query.Out }
+ Write-Output "Native AOT returned a controlled WSL query failure: $($query.Err.Trim())"
+} else {
+ throw "Native AOT query failed unexpectedly ($($query.Code)): $($query.Err)"
+}
+Write-Output 'Native AOT help, argument handling, and query/error smoke tests passed.'