127 lines
4.0 KiB
C#
127 lines
4.0 KiB
C#
using System.IO;
|
|
using System.Net.Http;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace ClaudeDo.Releases;
|
|
|
|
public static partial class SelfUpdater
|
|
{
|
|
[GeneratedRegex(@"^ClaudeDo\.Installer-(?<version>[\d\.]+)\.exe$", RegexOptions.IgnoreCase)]
|
|
private static partial Regex InstallerAssetRegex();
|
|
|
|
public static InstallerAssetMatch? FindInstallerAsset(IEnumerable<ReleaseAsset> 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<SelfUpdateDecision> 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<bool> HandleReplaceSelfAsync(
|
|
string oldPath,
|
|
string currentExePath,
|
|
Func<string, bool> 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<string?> DownloadAndVerifyAsync(
|
|
IReleaseClient releases,
|
|
ReleaseAsset installerAsset,
|
|
ReleaseAsset checksumsAsset,
|
|
string tempDir,
|
|
IProgress<long> 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<long>(_ => { }), 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;
|
|
}
|
|
}
|