using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.Win32; namespace LanMountainDesktop.Launcher.Infrastructure; internal enum DotNetRuntimeArchitecture { X64, X86 } internal sealed record DotNetRuntimeInfo( string Name, string Version, string Source, string? Location); internal sealed record DotNetRuntimeProbeOptions { public int RequiredMajorVersion { get; init; } = 10; public DotNetRuntimeArchitecture Architecture { get; init; } = DotNetRuntimeProbe.GetCurrentArchitecture(); public string? ProgramFilesPath { get; init; } public string? ProgramFilesX86Path { get; init; } public string? LocalAppDataPath { get; init; } public IReadOnlyList? DotNetHostCandidates { get; init; } public bool IncludeRegistry { get; init; } = true; public bool IncludeDotNetCli { get; init; } = true; } internal sealed record DotNetRuntimeProbeResult( bool IsAvailable, int RequiredMajorVersion, DotNetRuntimeArchitecture Architecture, string? DotNetHostPath, IReadOnlyList SearchedPaths, IReadOnlyList DetectedRuntimes, string Message) { public Dictionary ToDetails(string prefix = "dotnetRuntime") { return new Dictionary(StringComparer.OrdinalIgnoreCase) { [$"{prefix}Available"] = IsAvailable.ToString(), [$"{prefix}RequiredMajorVersion"] = RequiredMajorVersion.ToString(), [$"{prefix}Architecture"] = Architecture.ToString(), [$"{prefix}DotNetHostPath"] = DotNetHostPath ?? string.Empty, [$"{prefix}SearchedPaths"] = string.Join(" | ", SearchedPaths), [$"{prefix}DetectedRuntimes"] = string.Join( " | ", DetectedRuntimes.Select(runtime => $"{runtime.Name} {runtime.Version} [{runtime.Source}{(string.IsNullOrWhiteSpace(runtime.Location) ? string.Empty : $": {runtime.Location}")}]")), [$"{prefix}Message"] = Message }; } } 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) { options ??= new DotNetRuntimeProbeOptions(); var searchedPaths = new List(); var detected = new List(); var requiredMajor = options.RequiredMajorVersion; 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)) { searchedPaths.Add(candidate); if (dotNetHostPath is null && File.Exists(candidate)) { dotNetHostPath = candidate; } } if (OperatingSystem.IsWindows() && options.IncludeRegistry) { foreach (var frameworkName in RequiredSharedFrameworkNames) { AddRegistryRuntimes(options.Architecture, frameworkName, detected); } } if (options.IncludeDotNetCli) { AddDotNetCliRuntimes(dotNetHostPath, detected); } var isAvailable = detected.Any(runtime => string.Equals(runtime.Name, RequiredSharedFrameworkName, StringComparison.OrdinalIgnoreCase) && IsRequiredMajor(runtime.Version, requiredMajor)); var message = isAvailable ? $".NET {requiredMajor} runtime found for {options.Architecture}." : $".NET {requiredMajor} runtime was not found for {options.Architecture}."; return new DotNetRuntimeProbeResult( isAvailable, requiredMajor, options.Architecture, dotNetHostPath, searchedPaths .Where(path => !string.IsNullOrWhiteSpace(path)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(), detected .DistinctBy(runtime => $"{runtime.Name}|{runtime.Version}|{runtime.Source}|{runtime.Location}", StringComparer.OrdinalIgnoreCase) .OrderBy(runtime => runtime.Name, StringComparer.OrdinalIgnoreCase) .ThenBy(runtime => runtime.Version, StringComparer.OrdinalIgnoreCase) .ToList(), message); } public static DotNetRuntimeArchitecture GetCurrentArchitecture() { return RuntimeInformation.ProcessArchitecture switch { Architecture.X86 => DotNetRuntimeArchitecture.X86, _ => DotNetRuntimeArchitecture.X64 }; } public static string? FindDotNetHostPath(DotNetRuntimeProbeOptions? options = null) { options ??= new DotNetRuntimeProbeOptions(); return EnumerateDotNetHostCandidates(options).FirstOrDefault(File.Exists); } public static bool IsFrameworkDependentWindowsApp(string executablePath) { if (!OperatingSystem.IsWindows() || string.IsNullOrWhiteSpace(executablePath)) { return false; } var directory = Path.GetDirectoryName(Path.GetFullPath(executablePath)); if (string.IsNullOrWhiteSpace(directory)) { return false; } var appName = Path.GetFileNameWithoutExtension(executablePath); var runtimeConfigPath = Path.Combine(directory, $"{appName}.runtimeconfig.json"); if (!File.Exists(runtimeConfigPath)) { return false; } return !File.Exists(Path.Combine(directory, "coreclr.dll")) && !File.Exists(Path.Combine(directory, "hostfxr.dll")) && !File.Exists(Path.Combine(directory, "hostpolicy.dll")) && !File.Exists(Path.Combine(directory, "System.Private.CoreLib.dll")); } private static IEnumerable EnumerateDotNetInstallRoots(DotNetRuntimeProbeOptions options) { var programFilesRoot = options.Architecture == DotNetRuntimeArchitecture.X86 ? GetProgramFilesX86Path(options) : GetProgramFilesPath(options); 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) { if (options.DotNetHostCandidates is not null) { foreach (var candidate in options.DotNetHostCandidates) { if (!string.IsNullOrWhiteSpace(candidate)) { yield return Path.GetFullPath(candidate); } } yield break; } var programFilesRoot = options.Architecture == DotNetRuntimeArchitecture.X86 ? GetProgramFilesX86Path(options) : GetProgramFilesPath(options); 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) { if (!string.IsNullOrWhiteSpace(options.ProgramFilesPath)) { return Path.GetFullPath(options.ProgramFilesPath); } return Environment.GetEnvironmentVariable("ProgramW6432") ?? Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); } private static string GetProgramFilesX86Path(DotNetRuntimeProbeOptions options) { if (!string.IsNullOrWhiteSpace(options.ProgramFilesX86Path)) { return Path.GetFullPath(options.ProgramFilesX86Path); } return Environment.GetEnvironmentVariable("ProgramFiles(x86)") ?? 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, string source, List detected) { if (!Directory.Exists(sharedFrameworkDirectory)) { return; } foreach (var directory in Directory.GetDirectories(sharedFrameworkDirectory)) { var version = Path.GetFileName(directory); if (!string.IsNullOrWhiteSpace(version)) { detected.Add(new DotNetRuntimeInfo(sharedFrameworkName, version, source, directory)); } } } private static void AddRegistryRuntimes( DotNetRuntimeArchitecture architecture, string sharedFrameworkName, List detected) { try { var registryView = architecture == DotNetRuntimeArchitecture.X86 ? RegistryView.Registry32 : RegistryView.Registry64; using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, registryView); using var key = baseKey.OpenSubKey( $@"SOFTWARE\dotnet\Setup\InstalledVersions\{(architecture == DotNetRuntimeArchitecture.X86 ? "x86" : "x64")}\sharedfx\{sharedFrameworkName}"); if (key is null) { return; } foreach (var valueName in key.GetValueNames()) { if (key.GetValue(valueName) is not null) { detected.Add(new DotNetRuntimeInfo(sharedFrameworkName, valueName, "registry", key.Name)); } } } catch (Exception ex) { Logger.Warn($"Failed to inspect .NET runtime registry keys: {ex.Message}"); } } private static void AddDotNetCliRuntimes( string? dotNetHostPath, List detected) { if (string.IsNullOrWhiteSpace(dotNetHostPath) || !File.Exists(dotNetHostPath)) { return; } try { using var process = new Process(); process.StartInfo = new ProcessStartInfo { FileName = dotNetHostPath, Arguments = "--list-runtimes", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; process.Start(); var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(3000); foreach (var line in output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) { var parsed = ParseListRuntimeLine(line); if (parsed is not null && RequiredSharedFrameworkNames.Contains(parsed.Value.Name, StringComparer.OrdinalIgnoreCase)) { detected.Add(new DotNetRuntimeInfo( parsed.Value.Name, parsed.Value.Version, "dotnet-cli", parsed.Value.Location)); } } } catch (Exception ex) { Logger.Warn($"Failed to inspect .NET runtimes via dotnet CLI: {ex.Message}"); } } private static (string Name, string Version, string? Location)? ParseListRuntimeLine(string line) { var firstSpace = line.IndexOf(' '); if (firstSpace <= 0 || firstSpace + 1 >= line.Length) { return null; } var secondSpace = line.IndexOf(' ', firstSpace + 1); if (secondSpace <= firstSpace) { return null; } var name = line[..firstSpace].Trim(); var version = line[(firstSpace + 1)..secondSpace].Trim(); var location = line[(secondSpace + 1)..].Trim().Trim('[', ']'); return (name, version, string.IsNullOrWhiteSpace(location) ? null : location); } private static bool IsRequiredMajor(string version, int requiredMajor) { var dotIndex = version.IndexOf('.'); var majorText = dotIndex < 0 ? version : version[..dotIndex]; return int.TryParse(majorText, out var major) && major == requiredMajor; } }