using System.IO; using System.Net.Http; using System.Text.RegularExpressions; namespace ClaudeDo.Releases; public static partial class SelfUpdater { [GeneratedRegex(@"^ClaudeDo\.Installer-(?[\d\.]+)\.exe$", RegexOptions.IgnoreCase)] private static partial Regex InstallerAssetRegex(); public static InstallerAssetMatch? FindInstallerAsset(IEnumerable assets) { foreach (var asset in assets) { var m = InstallerAssetRegex().Match(asset.Name); if (m.Success) { return new InstallerAssetMatch(asset, m.Groups["version"].Value); } } return null; } public static async Task DecideUpdateAsync( IReleaseClient releases, string currentVersion, CancellationToken ct) { GiteaRelease? release; try { release = await releases.GetLatestReleaseAsync(ct); } catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) { return new SelfUpdateDecision(SelfUpdateDecisionKind.NoUpdate); } if (release is null) return new SelfUpdateDecision(SelfUpdateDecisionKind.NoUpdate); var match = FindInstallerAsset(release.Assets); if (match is null) return new SelfUpdateDecision(SelfUpdateDecisionKind.NoUpdate); var cmp = VersionComparer.Compare(match.Version, currentVersion); if (!cmp.IsNewer) return new SelfUpdateDecision(SelfUpdateDecisionKind.NoUpdate); var checksums = release.Assets.FirstOrDefault( a => string.Equals(a.Name, "checksums.txt", StringComparison.OrdinalIgnoreCase)); return new SelfUpdateDecision( SelfUpdateDecisionKind.UpdateAvailable, LatestVersion: match.Version, InstallerAsset: match.Asset, ChecksumsAsset: checksums); } public static async Task HandleReplaceSelfAsync( string oldPath, string currentExePath, Func launchProcess, int maxWaitMs = 5000) { var deadline = DateTime.UtcNow.AddMilliseconds(maxWaitMs); while (DateTime.UtcNow < deadline) { try { if (File.Exists(oldPath)) { File.Delete(oldPath); } break; } catch (IOException) { await Task.Delay(100); } catch (UnauthorizedAccessException) { await Task.Delay(100); } } if (File.Exists(oldPath)) { return false; } File.Copy(currentExePath, oldPath, overwrite: false); return launchProcess(oldPath); } public static async Task DownloadAndVerifyAsync( IReleaseClient releases, ReleaseAsset installerAsset, ReleaseAsset checksumsAsset, string tempDir, IProgress progress, CancellationToken ct) { Directory.CreateDirectory(tempDir); var installerPath = Path.Combine(tempDir, installerAsset.Name); var checksumsPath = Path.Combine(tempDir, "checksums.txt"); try { await releases.DownloadAsync(installerAsset.BrowserDownloadUrl, installerPath, progress, ct); await releases.DownloadAsync(checksumsAsset.BrowserDownloadUrl, checksumsPath, new Progress(_ => { }), ct); } catch (Exception ex) when (ex is HttpRequestException or IOException or TaskCanceledException) { return null; } var checksumsText = await File.ReadAllTextAsync(checksumsPath, ct); var map = ChecksumVerifier.ParseChecksumsFile(checksumsText); if (!map.TryGetValue(installerAsset.Name, out var expected)) return null; return ChecksumVerifier.Verify(installerPath, expected) ? installerPath : null; } }