zcbot/windows-node/Zcbot.WindowsNode/Runtime/SoftwareLocationService.cs

331 lines
13 KiB
C#

using Microsoft.Win32;
using System.Diagnostics;
using System.Security;
using System.Text.RegularExpressions;
namespace Zcbot.WindowsNode;
internal static class SoftwareLocationService
{
private const string SettingsKey = @"Software\Zcbot\WindowsNode\Software";
internal static SoftwareLocation ResolveLocation(SoftwareDefinition definition)
{
var configured = ReadConfiguredPath(definition.Id);
if (!string.IsNullOrWhiteSpace(configured))
{
return Location(definition, configured, "界面配置", strict: true);
}
if (definition.EnvironmentVariable is not null)
{
var environment = Environment.GetEnvironmentVariable(definition.EnvironmentVariable);
if (!string.IsNullOrWhiteSpace(environment))
{
return Location(
definition,
environment,
$"环境变量 {definition.EnvironmentVariable}",
strict: true);
}
}
return definition.Id switch
{
"origin" => DetectOrigin(definition),
"ansys" => DetectAnsys(definition),
"blender" => DetectBlender(definition),
_ => new SoftwareLocation(false, null, "自动检测", "没有可用的自动检测规则。"),
};
}
internal static void SaveConfiguredPath(SoftwareDefinition definition, string path)
{
var location = Location(definition, path, "界面配置", strict: true);
if (!location.Available || location.Path is null)
{
throw new InvalidDataException(location.Detail);
}
using var key = Registry.CurrentUser.CreateSubKey(SettingsKey, writable: true)
?? throw new InvalidOperationException("无法保存专业软件设置。");
key.SetValue(definition.Id, location.Path, RegistryValueKind.String);
}
internal static void ClearConfiguredPath(SoftwareDefinition definition)
{
using var key = Registry.CurrentUser.OpenSubKey(SettingsKey, writable: true);
key?.DeleteValue(definition.Id, throwOnMissingValue: false);
}
internal static void ApplyProcessEnvironment(ProcessStartInfo startInfo, string? runtimeId)
{
if (runtimeId is null)
{
return;
}
var definition = SoftwareCatalog.Definitions.SingleOrDefault(
item => item.RuntimeId.Equals(runtimeId, StringComparison.Ordinal));
if (definition?.EnvironmentVariable is null)
{
return;
}
var location = ResolveLocation(definition);
if (location.Available && location.Path is not null)
{
startInfo.Environment[definition.EnvironmentVariable] = location.Path;
}
}
private static SoftwareLocation DetectOrigin(SoftwareDefinition definition)
{
var candidates = new List<(string Path, string Source)>();
try
{
using var clsid = Registry.ClassesRoot.OpenSubKey(@"Origin.ApplicationSI\CLSID");
var clsidValue = clsid?.GetValue(null) as string;
if (!string.IsNullOrWhiteSpace(clsidValue))
{
using var server = Registry.ClassesRoot.OpenSubKey($@"CLSID\{clsidValue}\LocalServer32");
var executable = ExecutableFromCommand(server?.GetValue(null) as string);
if (executable is not null)
{
candidates.Add((executable, "COM 注册"));
}
}
}
catch (Exception exception) when (
exception is IOException or SecurityException or UnauthorizedAccessException)
{
// Continue with uninstall metadata and standard install directories.
}
candidates.AddRange(UninstallLocations("Origin")
.SelectMany(OriginExecutables)
.Select(path => (path, "注册表")));
foreach (var variable in new[] { "ProgramFiles", "ProgramW6432" })
{
var root = Environment.GetEnvironmentVariable(variable);
if (string.IsNullOrWhiteSpace(root)) continue;
var originRoot = Path.Combine(root, "OriginLab");
if (!Directory.Exists(originRoot)) continue;
try
{
candidates.AddRange(Directory.EnumerateDirectories(originRoot)
.OrderByDescending(item => item, StringComparer.OrdinalIgnoreCase)
.SelectMany(OriginExecutables)
.Select(path => (path, "标准目录")));
}
catch (Exception exception) when (
exception is IOException or SecurityException or UnauthorizedAccessException)
{
// Continue with candidates already collected from other sources.
}
}
return FirstLocation(definition, candidates);
}
private static IEnumerable<string> OriginExecutables(string location)
{
if (File.Exists(location))
{
yield return location;
yield break;
}
if (!Directory.Exists(location)) yield break;
IEnumerable<string> files;
try
{
files = Directory.EnumerateFiles(location, "Origin*.exe", SearchOption.TopDirectoryOnly)
.OrderByDescending(item => item, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
catch (Exception exception) when (
exception is IOException or SecurityException or UnauthorizedAccessException)
{
yield break;
}
foreach (var file in files) yield return file;
}
private static SoftwareLocation DetectAnsys(SoftwareDefinition definition)
{
var candidates = new List<(string Path, string Source)>();
foreach (var location in UninstallLocations("ANSYS"))
{
candidates.Add((location, "注册表"));
candidates.Add((Path.Combine(location, "v242"), "注册表"));
}
candidates.Add((@"C:\Program Files\ANSYS Inc\v242", "标准目录"));
return FirstLocation(definition, candidates);
}
private static SoftwareLocation DetectBlender(SoftwareDefinition definition)
{
var candidates = new List<(string Path, string Source)>();
var path = FindOnPath("blender.exe");
if (path is not null)
{
candidates.Add((path, "PATH"));
}
candidates.AddRange(UninstallLocations("Blender")
.Select(location => (Path.Combine(location, "blender.exe"), "注册表")));
foreach (var variable in new[] { "ProgramFiles", "ProgramW6432" })
{
var root = Environment.GetEnvironmentVariable(variable);
if (string.IsNullOrWhiteSpace(root)) continue;
var blenderRoot = Path.Combine(root, "Blender Foundation");
if (!Directory.Exists(blenderRoot)) continue;
candidates.AddRange(Directory.EnumerateDirectories(blenderRoot, "Blender *")
.OrderByDescending(item => item, StringComparer.OrdinalIgnoreCase)
.Select(item => (Path.Combine(item, "blender.exe"), "标准目录")));
}
return FirstLocation(definition, candidates);
}
private static SoftwareLocation FirstLocation(
SoftwareDefinition definition,
IEnumerable<(string Path, string Source)> candidates)
{
foreach (var candidate in candidates)
{
var location = Location(definition, candidate.Path, candidate.Source, strict: false);
if (location.Available)
{
return location;
}
}
return new SoftwareLocation(false, null, "自动检测", "没有找到有效的安装位置。");
}
private static SoftwareLocation Location(
SoftwareDefinition definition,
string value,
string source,
bool strict)
{
try
{
var path = Path.GetFullPath(Environment.ExpandEnvironmentVariables(value.Trim().Trim('"')));
var required = definition.PathKind switch
{
SoftwarePathKind.Executable => path,
SoftwarePathKind.Directory => Path.Combine(path, definition.RequiredRelativePath!),
_ => path,
};
var valid = File.Exists(required)
&& (definition.PathKind != SoftwarePathKind.Executable
|| MatchesExecutable(
Path.GetFileName(required),
definition.RequiredRelativePath!));
return valid
? new SoftwareLocation(true, path, source, $"已检测到 {required}")
: new SoftwareLocation(
false,
strict ? path : null,
source,
$"指定位置缺少 {definition.RequiredRelativePath}。");
}
catch (Exception exception) when (
exception is ArgumentException or IOException or NotSupportedException or SecurityException)
{
return new SoftwareLocation(false, null, source, exception.Message);
}
}
private static bool MatchesExecutable(string filename, string pattern)
{
if (!pattern.Contains('*'))
{
return filename.Equals(pattern, StringComparison.OrdinalIgnoreCase);
}
var parts = pattern.Split('*', 2);
return filename.StartsWith(parts[0], StringComparison.OrdinalIgnoreCase)
&& filename.EndsWith(parts[1], StringComparison.OrdinalIgnoreCase)
&& filename.Length >= parts[0].Length + parts[1].Length;
}
private static string? ReadConfiguredPath(string id)
{
using var key = Registry.CurrentUser.OpenSubKey(SettingsKey);
return key?.GetValue(id) as string;
}
private static IReadOnlyList<string> UninstallLocations(string displayNameFragment)
{
var result = new List<string>();
foreach (var hive in new[] { RegistryHive.LocalMachine, RegistryHive.CurrentUser })
{
foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
{
RegistryKey? baseKey = null;
RegistryKey? uninstall = null;
try
{
baseKey = RegistryKey.OpenBaseKey(hive, view);
uninstall = baseKey.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall");
if (uninstall is null) continue;
foreach (var name in uninstall.GetSubKeyNames())
{
using var entry = uninstall.OpenSubKey(name);
var displayName = entry?.GetValue("DisplayName") as string;
var location = entry?.GetValue("InstallLocation") as string;
if (!string.IsNullOrWhiteSpace(displayName)
&& displayName.Contains(displayNameFragment, StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrWhiteSpace(location))
{
result.Add(location);
}
}
}
catch (Exception exception) when (
exception is IOException or SecurityException or UnauthorizedAccessException)
{
// A registry view can be unavailable to the current node account.
}
finally
{
uninstall?.Dispose();
baseKey?.Dispose();
}
}
}
return result.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
private static string? FindOnPath(string filename)
{
var path = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrWhiteSpace(path)) return null;
foreach (var directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
try
{
var candidate = Path.Combine(directory.Trim().Trim('"'), filename);
if (File.Exists(candidate)) return Path.GetFullPath(candidate);
}
catch (Exception exception) when (
exception is ArgumentException or IOException or NotSupportedException)
{
// Ignore malformed PATH entries and continue with the next one.
}
}
return null;
}
private static string? ExecutableFromCommand(string? command)
{
if (string.IsNullOrWhiteSpace(command)) return null;
var value = Environment.ExpandEnvironmentVariables(command.Trim().TrimEnd('\0'));
var match = Regex.Match(
value,
"^\\s*(?:\"(?<quoted>[^\"]+\\.exe)\"|(?<plain>.+?\\.exe))(?:\\s|$)",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success) return null;
var candidate = match.Groups["quoted"].Success
? match.Groups["quoted"].Value
: match.Groups["plain"].Value;
return File.Exists(candidate) ? Path.GetFullPath(candidate) : null;
}
}