feat: serialize package pointer transactions

This commit is contained in:
codex
2026-08-06 09:21:17 +03:00
parent 961d8a662c
commit 9a5c578af4
3 changed files with 33 additions and 10 deletions
@@ -7,7 +7,9 @@ public sealed class Sha256FileHasher : IFileHasher
{ public async Task<string> Sha256Async(string path, CancellationToken cancellationToken) { await using var s = File.OpenRead(path); return Convert.ToHexString(await SHA256.HashDataAsync(s, cancellationToken)); } }
public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalPackageStore, IPackageActivator, IRollbackManager, IPackageSynchronizer, IPackageMaterializer, ILastKnownGoodPackageResolver, IPackageRequirementCacheBinder
{
private sealed record Pointer(string Current, string? Previous);
// Machine-process local serialization only; package roots must remain local.
private static readonly SemaphoreSlim PointerGate = new(1, 1);
private sealed record Pointer(string Current, string? Previous, int SchemaVersion = 1, long Generation = 0, string? ActivationOperationId = null, DateTimeOffset? ActivatedAtUtc = null);
private sealed record CacheBinding(string PackageId, string Product, int ProductYear, string Architecture, string Channel, string RequiredVersion, string RosterIdentity);
private const string StoredManifestFileName = "package.manifest.json";
private const string StoredBindingFileName = "package.binding.json";
@@ -17,29 +19,34 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP
{
if (!File.Exists(PointerPath(id))) return null;
await using var stream = File.OpenRead(PointerPath(id));
return await JsonSerializer.DeserializeAsync<Pointer>(stream, cancellationToken: ct);
var pointer = await JsonSerializer.DeserializeAsync<Pointer>(stream, cancellationToken: ct);
return pointer is null || pointer.SchemaVersion != 1 || string.IsNullOrWhiteSpace(pointer.Current) ? null : pointer;
}
public async Task<string?> GetCurrentVersionAsync(string id, CancellationToken ct) => (await ReadPointer(id, ct))?.Current;
public async Task<string?> GetPreviousVersionAsync(string id, CancellationToken ct) => (await ReadPointer(id, ct))?.Previous;
public async Task<ActivationResult> ActivateAsync(string id, string version, CancellationToken ct)
{
var dir = VersionDirectory(id, version); if (!Directory.Exists(dir)) throw new DirectoryNotFoundException(dir);
Directory.CreateDirectory(PackageRoot(id)); var prior = await ReadPointer(id, ct);
var temp = PointerPath(id) + ".tmp"; await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(new Pointer(version, prior?.Current)), ct); File.Move(temp, PointerPath(id), true);
return new(version, prior?.Current);
await PointerGate.WaitAsync(ct); try
{
var dir = VersionDirectory(id, version); if (!Directory.Exists(dir)) throw new DirectoryNotFoundException(dir);
Directory.CreateDirectory(PackageRoot(id)); var prior = await ReadPointer(id, ct); var operationId=Guid.NewGuid().ToString("N");
var temp = PointerPath(id) + "." + operationId + ".tmp"; await using (var stream=new FileStream(temp,FileMode.CreateNew,FileAccess.Write,FileShare.None)) { await JsonSerializer.SerializeAsync(stream,new Pointer(version, prior?.Current,1,(prior?.Generation??0)+1,operationId,DateTimeOffset.UtcNow),cancellationToken:ct); await stream.FlushAsync(ct); }
File.Move(temp, PointerPath(id), true); var verified=await ReadPointer(id,ct); if (verified?.Current!=version || verified.ActivationOperationId!=operationId) throw new InvalidOperationException("POINTER_REVALIDATION_FAILED");
return new(version, prior?.Current);
} finally { PointerGate.Release(); }
}
public async Task<PackageActivationResult> ActivateImmutableAsync(ImmutableLocalPackage package, PackageRequirement requirement, CancellationToken ct)
{
if (!string.Equals(package.PackageId, requirement.PackageId, StringComparison.Ordinal) || !string.Equals(package.Version, requirement.RequiredVersion, StringComparison.Ordinal)) return new("Blocked", null);
var prior = await ReadPointer(package.PackageId, ct);
if (prior?.Current == package.Version) return new("AlreadyActiveExact", new(Guid.NewGuid().ToString("N"), package.PackageId, package.Version, prior.Previous, DateTimeOffset.UtcNow));
if (prior?.Current == package.Version) return new("AlreadyActiveExact", new(prior.ActivationOperationId ?? Guid.NewGuid().ToString("N"), package.PackageId, package.Version, prior.Previous, prior.ActivatedAtUtc ?? DateTimeOffset.UtcNow, prior.Generation, PackageRoot(package.PackageId)));
var activated = await ActivateAsync(package.PackageId, package.Version, ct);
return new("Activated", new(Guid.NewGuid().ToString("N"), package.PackageId, activated.CurrentVersion, activated.PreviousVersion, DateTimeOffset.UtcNow));
var after=await ReadPointer(package.PackageId,ct); return new("Activated", new(after!.ActivationOperationId!, package.PackageId, activated.CurrentVersion, activated.PreviousVersion, after.ActivatedAtUtc ?? DateTimeOffset.UtcNow, after.Generation, PackageRoot(package.PackageId)));
}
public async Task<PackageRollbackResult> RollbackAsync(PackageActivationReceipt receipt, CancellationToken ct)
{
var current=await GetCurrentVersionAsync(receipt.PackageId,ct);
if (!string.Equals(current,receipt.Version,StringComparison.Ordinal)) return new("RollbackBlockedStateChanged",current);
var pointer=await ReadPointer(receipt.PackageId,ct); if (pointer is null || !string.Equals(current,receipt.Version,StringComparison.Ordinal) || pointer.ActivationOperationId!=receipt.OperationId || pointer.Generation!=receipt.PointerGeneration) return new("RollbackBlockedStateChanged",current);
if (string.IsNullOrWhiteSpace(receipt.PreviousVersion)) return new("RollbackNotApplicable",current);
var restored=await ActivateAsync(receipt.PackageId,receipt.PreviousVersion,ct);
return new("RolledBack",restored.CurrentVersion);