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 OriginExecutables(string location) { if (File.Exists(location)) { yield return location; yield break; } if (!Directory.Exists(location)) yield break; IEnumerable 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 UninstallLocations(string displayNameFragment) { var result = new List(); 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*(?:\"(?[^\"]+\\.exe)\"|(?.+?\\.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; } }