sqlinsight-pro/src/SQLInsight.Updates/UpdateChecker.cs
Claude 3121b598f9
Some checks are pending
CI / Build og unit tests (push) Waiting to run
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
Fase 3: signed updates of rule packs and the application
Rule packs (support dates and newest CU per release) are published in a
signed feed and hot-loaded without a restart; the verified pack is
reloaded at startup. New versions are announced (Notify) or downloaded
and staged by the unprivileged agent (Automatic), and installed by a
privileged apply-update task that re-verifies signature and hash in an
admin-only folder, keeps appsettings/web.config, health-checks the
services and rolls back on failure. Stable and preview channels,
manifest expiry and rollback protection, https or folder/share feeds
for offline sites. Vendor tool gains rulepack and publish; the Hub can
serve the feed; the web UI gets a System page with update, rule pack,
license and exactly what is sent to the Hub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
2026-09-29 09:19:41 +00:00

92 lines
4.2 KiB
C#

using System.Text;
using SQLInsight.Licensing;
using SQLInsight.Rules.Lifecycle;
namespace SQLInsight.Updates;
public sealed record CheckedManifest(UpdateManifest Manifest, string Token);
/// <summary>
/// The unprivileged half of updating, run by the agent: reads the signed manifest, installs newer rule packs, and
/// downloads and stages application packages. It never replaces program files; <see cref="UpdateApplier"/> does that.
/// </summary>
public sealed class UpdateChecker(UpdateFeed feed, TrustedKeys keys, UpdateDirectories directories, string channel, TimeProvider time)
{
public string Channel { get; } = channel;
public async Task<CheckedManifest> FetchAsync(CancellationToken cancellationToken)
{
var token = await feed.ReadManifestAsync(Channel, cancellationToken).ConfigureAwait(false);
var manifest = ManifestVerifier.Verify(token, keys, Channel, time.GetUtcNow(), directories.LastManifest(Channel));
directories.RememberManifest(Channel, manifest.PublishedAt);
return new CheckedManifest(manifest, token);
}
/// <summary>Downloads the manifest's rule pack if it is newer than <paramref name="current"/>. Returns the new pack or null.</summary>
public async Task<RulePack?> UpdateRulePackAsync(CheckedManifest source, RulePack current, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(current);
if (source.Manifest.RulePack is not { } reference || !Version.TryParse(reference.Version, out var offered) || offered <= current.Version)
{
return null;
}
var bytes = await feed.DownloadAsync(reference.File, cancellationToken).ConfigureAwait(false);
RulePack pack;
try
{
pack = RulePack.Parse(bytes, $"regelpakke fra {source.Manifest.Channel}-kanalen, {source.Manifest.PublishedAt:yyyy-MM-dd}");
}
catch (InvalidDataException ex)
{
throw new UpdateException(ex.Message, ex);
}
if (pack.Version != offered)
{
throw new UpdateException($"Regelpakkens version ({pack.Version}) svarer ikke til manifestet ({offered}).");
}
new RulePackStore(directories).Save(bytes, source.Token);
return pack;
}
/// <summary>The release in the manifest if it is newer than the running version, otherwise null.</summary>
public static AppRelease? NewerRelease(UpdateManifest manifest, AppVersion installed)
{
ArgumentNullException.ThrowIfNull(manifest);
return manifest.Release is { } release && AppVersion.Parse(release.Version) > installed ? release : null;
}
/// <summary>
/// Downloads the package for this platform into the pending folder, where the privileged updater picks it up.
/// Returns false if there is nothing newer than <paramref name="installed"/>.
/// </summary>
public async Task<bool> StageAsync(CheckedManifest source, AppVersion installed, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(source);
if (NewerRelease(source.Manifest, installed) is not { } release)
{
return false;
}
var asset = release.Assets.FirstOrDefault(a => string.Equals(a.Platform, Platform.Current, StringComparison.OrdinalIgnoreCase))
?? throw new UpdateException($"Version {release.Version} har ingen pakke til {Platform.Current}.");
var package = Path.Combine(directories.Pending, "package.zip");
var manifestFile = Path.Combine(directories.Pending, UpdateFeed.ManifestFile);
if (File.Exists(package) && File.Exists(manifestFile) && await File.ReadAllTextAsync(manifestFile, cancellationToken).ConfigureAwait(false) == source.Token)
{
return true;
}
Directory.CreateDirectory(directories.Pending);
File.Delete(manifestFile);
var download = package + ".download";
await feed.DownloadToFileAsync(asset.File, download, cancellationToken).ConfigureAwait(false);
File.Move(download, package, overwrite: true);
UpdateDirectories.WriteAtomic(manifestFile, Encoding.UTF8.GetBytes(source.Token));
return true;
}
}