From 12f0caafc735aae8dc9c8d19f2c0829288106280 Mon Sep 17 00:00:00 2001 From: lincube Date: Mon, 25 May 2026 01:24:18 +0800 Subject: [PATCH] =?UTF-8?q?fix.=E7=BB=A7=E7=BB=AD=E4=BF=AE=E5=A4=8D=20.NET?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DotNetRuntimeProbe.cs | 80 +++++++++++-- .../DotNetRuntimeProbeTests.cs | 109 +++++++++++++++++- .../installer/LanMountainDesktop.iss | 8 +- 3 files changed, 181 insertions(+), 16 deletions(-) diff --git a/LanMountainDesktop.Launcher/Services/DotNetRuntimeProbe.cs b/LanMountainDesktop.Launcher/Services/DotNetRuntimeProbe.cs index 87fbf55..04c9a41 100644 --- a/LanMountainDesktop.Launcher/Services/DotNetRuntimeProbe.cs +++ b/LanMountainDesktop.Launcher/Services/DotNetRuntimeProbe.cs @@ -26,6 +26,8 @@ internal sealed record DotNetRuntimeProbeOptions public string? ProgramFilesX86Path { get; init; } + public string? LocalAppDataPath { get; init; } + public IReadOnlyList? DotNetHostCandidates { get; init; } public bool IncludeRegistry { get; init; } = true; @@ -63,6 +65,13 @@ internal sealed record DotNetRuntimeProbeResult( internal static class DotNetRuntimeProbe { public const string RequiredSharedFrameworkName = "Microsoft.NETCore.App"; + public const string WindowsDesktopSharedFrameworkName = "Microsoft.WindowsDesktop.App"; + + private static readonly string[] RequiredSharedFrameworkNames = + [ + RequiredSharedFrameworkName, + WindowsDesktopSharedFrameworkName + ]; public static DotNetRuntimeProbeResult Probe(DotNetRuntimeProbeOptions? options = null) { @@ -71,10 +80,25 @@ internal static class DotNetRuntimeProbe var searchedPaths = new List(); var detected = new List(); var requiredMajor = options.RequiredMajorVersion; - var sharedFrameworkDirectory = GetSharedFrameworkDirectory(options, RequiredSharedFrameworkName); - searchedPaths.Add(sharedFrameworkDirectory); - AddDirectoryRuntimes(sharedFrameworkDirectory, RequiredSharedFrameworkName, "shared-framework-directory", detected); + var localAppDataRoot = GetLocalAppDataPath(options); + var perUserDotnetRoot = !string.IsNullOrWhiteSpace(localAppDataRoot) + ? Path.Combine(localAppDataRoot, "dotnet") + : null; + + foreach (var frameworkName in RequiredSharedFrameworkNames) + { + foreach (var basePath in EnumerateDotNetInstallRoots(options)) + { + var sharedFrameworkDirectory = Path.Combine(basePath, "shared", frameworkName); + searchedPaths.Add(sharedFrameworkDirectory); + var isPerUser = perUserDotnetRoot is not null && + string.Equals(basePath, perUserDotnetRoot, StringComparison.OrdinalIgnoreCase); + AddDirectoryRuntimes(sharedFrameworkDirectory, frameworkName, + isPerUser ? "shared-framework-directory-per-user" : "shared-framework-directory", + detected); + } + } string? dotNetHostPath = null; foreach (var candidate in EnumerateDotNetHostCandidates(options)) @@ -88,12 +112,15 @@ internal static class DotNetRuntimeProbe if (OperatingSystem.IsWindows() && options.IncludeRegistry) { - AddRegistryRuntimes(options.Architecture, RequiredSharedFrameworkName, detected); + foreach (var frameworkName in RequiredSharedFrameworkNames) + { + AddRegistryRuntimes(options.Architecture, frameworkName, detected); + } } if (options.IncludeDotNetCli) { - AddDotNetCliRuntimes(dotNetHostPath, RequiredSharedFrameworkName, detected); + AddDotNetCliRuntimes(dotNetHostPath, detected); } var isAvailable = detected.Any(runtime => @@ -162,13 +189,23 @@ internal static class DotNetRuntimeProbe !File.Exists(Path.Combine(directory, "System.Private.CoreLib.dll")); } - private static string GetSharedFrameworkDirectory(DotNetRuntimeProbeOptions options, string sharedFrameworkName) + private static IEnumerable EnumerateDotNetInstallRoots(DotNetRuntimeProbeOptions options) { - var root = options.Architecture == DotNetRuntimeArchitecture.X86 + var programFilesRoot = options.Architecture == DotNetRuntimeArchitecture.X86 ? GetProgramFilesX86Path(options) : GetProgramFilesPath(options); - return Path.Combine(root, "dotnet", "shared", sharedFrameworkName); + yield return Path.Combine(programFilesRoot, "dotnet"); + + var localAppData = GetLocalAppDataPath(options); + if (!string.IsNullOrWhiteSpace(localAppData)) + { + var perUserDotnet = Path.Combine(localAppData, "dotnet"); + if (!string.Equals(perUserDotnet, Path.Combine(programFilesRoot, "dotnet"), StringComparison.OrdinalIgnoreCase)) + { + yield return perUserDotnet; + } + } } private static IEnumerable EnumerateDotNetHostCandidates(DotNetRuntimeProbeOptions options) @@ -186,11 +223,21 @@ internal static class DotNetRuntimeProbe yield break; } - var root = options.Architecture == DotNetRuntimeArchitecture.X86 + var programFilesRoot = options.Architecture == DotNetRuntimeArchitecture.X86 ? GetProgramFilesX86Path(options) : GetProgramFilesPath(options); - yield return Path.Combine(root, "dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"); + yield return Path.Combine(programFilesRoot, "dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"); + + var localAppData = GetLocalAppDataPath(options); + if (!string.IsNullOrWhiteSpace(localAppData)) + { + var perUserHost = Path.Combine(localAppData, "dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"); + if (!string.Equals(perUserHost, Path.Combine(programFilesRoot, "dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"), StringComparison.OrdinalIgnoreCase)) + { + yield return perUserHost; + } + } } private static string GetProgramFilesPath(DotNetRuntimeProbeOptions options) @@ -215,6 +262,16 @@ internal static class DotNetRuntimeProbe Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); } + private static string GetLocalAppDataPath(DotNetRuntimeProbeOptions options) + { + if (!string.IsNullOrWhiteSpace(options.LocalAppDataPath)) + { + return Path.GetFullPath(options.LocalAppDataPath); + } + + return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + } + private static void AddDirectoryRuntimes( string sharedFrameworkDirectory, string sharedFrameworkName, @@ -271,7 +328,6 @@ internal static class DotNetRuntimeProbe private static void AddDotNetCliRuntimes( string? dotNetHostPath, - string sharedFrameworkName, List detected) { if (string.IsNullOrWhiteSpace(dotNetHostPath) || !File.Exists(dotNetHostPath)) @@ -300,7 +356,7 @@ internal static class DotNetRuntimeProbe { var parsed = ParseListRuntimeLine(line); if (parsed is not null && - string.Equals(parsed.Value.Name, sharedFrameworkName, StringComparison.OrdinalIgnoreCase)) + RequiredSharedFrameworkNames.Contains(parsed.Value.Name, StringComparer.OrdinalIgnoreCase)) { detected.Add(new DotNetRuntimeInfo( parsed.Value.Name, diff --git a/LanMountainDesktop.Tests/DotNetRuntimeProbeTests.cs b/LanMountainDesktop.Tests/DotNetRuntimeProbeTests.cs index 6472c12..6f2f64d 100644 --- a/LanMountainDesktop.Tests/DotNetRuntimeProbeTests.cs +++ b/LanMountainDesktop.Tests/DotNetRuntimeProbeTests.cs @@ -8,14 +8,17 @@ public sealed class DotNetRuntimeProbeTests : IDisposable private readonly string _root; private readonly string _programFiles; private readonly string _programFilesX86; + private readonly string _localAppData; public DotNetRuntimeProbeTests() { _root = Path.Combine(Path.GetTempPath(), "LanMountainDesktop.DotNetRuntimeProbeTests", Guid.NewGuid().ToString("N")); _programFiles = Path.Combine(_root, "ProgramFiles"); _programFilesX86 = Path.Combine(_root, "ProgramFilesX86"); + _localAppData = Path.Combine(_root, "LocalAppData"); Directory.CreateDirectory(_programFiles); Directory.CreateDirectory(_programFilesX86); + Directory.CreateDirectory(_localAppData); } [Fact] @@ -61,6 +64,104 @@ public sealed class DotNetRuntimeProbeTests : IDisposable Assert.False(result.IsAvailable); } + [Fact] + public void Probe_DetectsPerUserRuntime() + { + CreateRuntime(_localAppData, "10.0.5", DotNetRuntimeProbe.RequiredSharedFrameworkName); + + var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64)); + + Assert.True(result.IsAvailable); + Assert.Contains(result.DetectedRuntimes, runtime => + runtime.Version == "10.0.5" && + runtime.Source == "shared-framework-directory-per-user"); + } + + [Fact] + public void Probe_DetectsWindowsDesktopRuntime() + { + CreateRuntime(_programFiles, "10.0.5", DotNetRuntimeProbe.WindowsDesktopSharedFrameworkName); + + var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64)); + + Assert.False(result.IsAvailable); + Assert.Contains(result.DetectedRuntimes, runtime => + runtime.Name == DotNetRuntimeProbe.WindowsDesktopSharedFrameworkName && + runtime.Version == "10.0.5"); + } + + [Fact] + public void Probe_DetectsPerUserWindowsDesktopRuntime() + { + CreateRuntime(_localAppData, "10.0.5", DotNetRuntimeProbe.WindowsDesktopSharedFrameworkName); + + var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64)); + + Assert.Contains(result.DetectedRuntimes, runtime => + runtime.Name == DotNetRuntimeProbe.WindowsDesktopSharedFrameworkName && + runtime.Version == "10.0.5" && + runtime.Source == "shared-framework-directory-per-user"); + } + + [Fact] + public void Probe_FindsDotNetHost_InPerUserPath() + { + var dotnetDir = Path.Combine(_localAppData, "dotnet"); + Directory.CreateDirectory(dotnetDir); + File.WriteAllText(Path.Combine(dotnetDir, "dotnet.exe"), string.Empty); + + var result = DotNetRuntimeProbe.Probe(new DotNetRuntimeProbeOptions + { + Architecture = DotNetRuntimeArchitecture.X64, + ProgramFilesPath = _programFiles, + ProgramFilesX86Path = _programFilesX86, + LocalAppDataPath = _localAppData, + IncludeRegistry = false, + IncludeDotNetCli = false + }); + + Assert.NotNull(result.DotNetHostPath); + Assert.Contains("LocalAppData", result.DotNetHostPath); + } + + [Fact] + public void Probe_PrefersProgramFilesHost_OverPerUserHost() + { + var systemDotnetDir = Path.Combine(_programFiles, "dotnet"); + Directory.CreateDirectory(systemDotnetDir); + File.WriteAllText(Path.Combine(systemDotnetDir, "dotnet.exe"), string.Empty); + + var perUserDotnetDir = Path.Combine(_localAppData, "dotnet"); + Directory.CreateDirectory(perUserDotnetDir); + File.WriteAllText(Path.Combine(perUserDotnetDir, "dotnet.exe"), string.Empty); + + var result = DotNetRuntimeProbe.Probe(new DotNetRuntimeProbeOptions + { + Architecture = DotNetRuntimeArchitecture.X64, + ProgramFilesPath = _programFiles, + ProgramFilesX86Path = _programFilesX86, + LocalAppDataPath = _localAppData, + IncludeRegistry = false, + IncludeDotNetCli = false + }); + + Assert.NotNull(result.DotNetHostPath); + Assert.Contains("ProgramFiles", result.DotNetHostPath); + } + + [Fact] + public void Probe_CombinesSystemAndPerUserRuntimes() + { + CreateRuntime(_programFiles, "10.0.5"); + CreateRuntime(_localAppData, "10.0.3"); + + var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64)); + + Assert.True(result.IsAvailable); + Assert.Contains(result.DetectedRuntimes, runtime => runtime.Version == "10.0.5"); + Assert.Contains(result.DetectedRuntimes, runtime => runtime.Version == "10.0.3"); + } + [Fact] public void ValidateDotNetRuntimePrerequisite_ReturnsStructuredFailure_WhenRuntimeIsMissing() { @@ -109,19 +210,21 @@ public sealed class DotNetRuntimeProbeTests : IDisposable Architecture = architecture, ProgramFilesPath = _programFiles, ProgramFilesX86Path = _programFilesX86, + LocalAppDataPath = _localAppData, DotNetHostCandidates = [], IncludeRegistry = false, IncludeDotNetCli = false }; } - private static void CreateRuntime(string programFilesRoot, string version) + private static void CreateRuntime(string root, string version, string? frameworkName = null) { + frameworkName ??= DotNetRuntimeProbe.RequiredSharedFrameworkName; Directory.CreateDirectory(Path.Combine( - programFilesRoot, + root, "dotnet", "shared", - DotNetRuntimeProbe.RequiredSharedFrameworkName, + frameworkName, version)); } diff --git a/LanMountainDesktop/installer/LanMountainDesktop.iss b/LanMountainDesktop/installer/LanMountainDesktop.iss index 6580abd..796b230 100644 --- a/LanMountainDesktop/installer/LanMountainDesktop.iss +++ b/LanMountainDesktop/installer/LanMountainDesktop.iss @@ -564,6 +564,11 @@ begin end; end; +function GetPerUserDotNetDesktopRuntimePath(): String; +begin + Result := ExpandConstant('{localappdata}\dotnet\shared\Microsoft.WindowsDesktop.App'); +end; + function GetDotNetRuntimeDownloadUrl(): String; begin if '{#MyAppArch}' = 'x64' then @@ -590,7 +595,8 @@ end; function IsDotNetDesktopRuntimeInstalled(): Boolean; begin - Result := IsDotNet10RuntimePresent(GetTargetDotNetDesktopRuntimePath()); + Result := IsDotNet10RuntimePresent(GetTargetDotNetDesktopRuntimePath()) or + IsDotNet10RuntimePresent(GetPerUserDotNetDesktopRuntimePath()); end; function DotNetDownloadProgress(