mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
fix.正在修复 .NET运行时问题
This commit is contained in:
345
LanMountainDesktop.Launcher/Services/DotNetRuntimeProbe.cs
Normal file
345
LanMountainDesktop.Launcher/Services/DotNetRuntimeProbe.cs
Normal file
@@ -0,0 +1,345 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Services;
|
||||
|
||||
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 IReadOnlyList<string>? 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<string> SearchedPaths,
|
||||
IReadOnlyList<DotNetRuntimeInfo> DetectedRuntimes,
|
||||
string Message)
|
||||
{
|
||||
public Dictionary<string, string> ToDetails(string prefix = "dotnetRuntime")
|
||||
{
|
||||
return new Dictionary<string, string>(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 static DotNetRuntimeProbeResult Probe(DotNetRuntimeProbeOptions? options = null)
|
||||
{
|
||||
options ??= new DotNetRuntimeProbeOptions();
|
||||
|
||||
var searchedPaths = new List<string>();
|
||||
var detected = new List<DotNetRuntimeInfo>();
|
||||
var requiredMajor = options.RequiredMajorVersion;
|
||||
var sharedFrameworkDirectory = GetSharedFrameworkDirectory(options, RequiredSharedFrameworkName);
|
||||
searchedPaths.Add(sharedFrameworkDirectory);
|
||||
|
||||
AddDirectoryRuntimes(sharedFrameworkDirectory, RequiredSharedFrameworkName, "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)
|
||||
{
|
||||
AddRegistryRuntimes(options.Architecture, RequiredSharedFrameworkName, detected);
|
||||
}
|
||||
|
||||
if (options.IncludeDotNetCli)
|
||||
{
|
||||
AddDotNetCliRuntimes(dotNetHostPath, RequiredSharedFrameworkName, 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 string GetSharedFrameworkDirectory(DotNetRuntimeProbeOptions options, string sharedFrameworkName)
|
||||
{
|
||||
var root = options.Architecture == DotNetRuntimeArchitecture.X86
|
||||
? GetProgramFilesX86Path(options)
|
||||
: GetProgramFilesPath(options);
|
||||
|
||||
return Path.Combine(root, "dotnet", "shared", sharedFrameworkName);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> 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 root = options.Architecture == DotNetRuntimeArchitecture.X86
|
||||
? GetProgramFilesX86Path(options)
|
||||
: GetProgramFilesPath(options);
|
||||
|
||||
yield return Path.Combine(root, "dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet");
|
||||
}
|
||||
|
||||
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 void AddDirectoryRuntimes(
|
||||
string sharedFrameworkDirectory,
|
||||
string sharedFrameworkName,
|
||||
string source,
|
||||
List<DotNetRuntimeInfo> 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<DotNetRuntimeInfo> 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,
|
||||
string sharedFrameworkName,
|
||||
List<DotNetRuntimeInfo> 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 &&
|
||||
string.Equals(parsed.Value.Name, sharedFrameworkName, StringComparison.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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user