using LanMountainDesktop.Shared.Contracts.Deployment; namespace LanDesktopPLONDS.Installer.Services; /// /// 检测已安装产品的信息:扫描部署目录,读取 .current 标记, /// 从目录名(app-{version}-{index})中解析 SemanticVersion。 /// internal sealed class InstalledProductInspector { /// /// 检测指定安装根目录下的已安装产品。 /// /// /// 启动器根目录(包含 app-* 子目录的父目录)。 /// /// /// 如果找到有效的已安装产品则返回 ,否则返回 null。 /// public InstalledProductInfo? Detect(string launcherRoot) { if (string.IsNullOrWhiteSpace(launcherRoot) || !Directory.Exists(launcherRoot)) { return null; } // 扫描所有部署目录(app-* 前缀) var candidates = Directory.GetDirectories(launcherRoot, DeploymentLayout.DeploymentDirectoryPrefix + "*", SearchOption.TopDirectoryOnly); InstalledProductInfo? best = null; foreach (var dir in candidates) { var dirName = Path.GetFileName(dir); // 跳过被标记为销毁或部分完成的部署 if (File.Exists(Path.Combine(dir, DeploymentLayout.DestroyMarkerFileName)) || File.Exists(Path.Combine(dir, DeploymentLayout.PartialMarkerFileName))) { continue; } // 从目录名解析版本号:app-{version}-{index} → 提取 version 部分 var version = ParseVersionFromDirectoryName(dirName); if (version is null) { continue; } var hasCurrent = File.Exists(Path.Combine(dir, DeploymentLayout.CurrentMarkerFileName)); // 优先选择 .current 标记的部署;相同标记下选最新版本 if (best is null || (hasCurrent && !best.HasCurrentMarker) || (hasCurrent == best.HasCurrentMarker && version > best.Version)) { best = new InstalledProductInfo( Version: version, DeploymentPath: dir, HasCurrentMarker: hasCurrent); } } return best; } /// /// 从部署目录名中解析语义版本号。 /// 目录名格式:app-{version}-{index},其中 version 可能包含预发布标签。 /// internal static SemanticVersion? ParseVersionFromDirectoryName(string directoryName) { if (string.IsNullOrWhiteSpace(directoryName)) { return null; } // 去掉 "app-" 前缀 if (!directoryName.StartsWith(DeploymentLayout.DeploymentDirectoryPrefix, StringComparison.OrdinalIgnoreCase)) { return null; } var withoutPrefix = directoryName[DeploymentLayout.DeploymentDirectoryPrefix.Length..]; // 分割为 [version, index] — index 是最后一个 "-{n}" 段 // version 本身可能包含预发布标签(如 1.2.3-beta.1),需要正确处理 var lastDash = withoutPrefix.LastIndexOf('-'); if (lastDash <= 0) { // 没有找到 "-" 分隔符,尝试将整个部分作为版本 return SemanticVersion.TryParse(withoutPrefix, out var sv) ? sv : null; } var versionPart = withoutPrefix[..lastDash]; return SemanticVersion.TryParse(versionPart, out var parsed) ? parsed : null; } } /// /// 已安装产品的信息。 /// internal sealed record InstalledProductInfo( SemanticVersion Version, string DeploymentPath, bool HasCurrentMarker);