331 lines
13 KiB
C#
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;
|
|
}
|
|
|
|
}
|