Compare commits
3
Commits
0e38d8fad2
...
8d32f8e7ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d32f8e7ae | ||
|
|
9fce6ee830 | ||
|
|
64fa1f5664 |
@@ -1,4 +1,5 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Launcher.Next.Core;
|
using Launcher.Next.Core;
|
||||||
@@ -8,22 +9,40 @@ namespace Launcher.Next.App;
|
|||||||
|
|
||||||
internal static class PackageLifecyclePilotCli
|
internal static class PackageLifecyclePilotCli
|
||||||
{
|
{
|
||||||
|
private sealed class Fault(PackageLifecycleFaultPoint point) : IPackageLifecycleFaultInjector
|
||||||
|
{ public void ThrowIfRequested(PackageLifecycleFaultPoint requested) { if(requested==point) throw new IOException($"INJECTED:{point}"); } }
|
||||||
|
|
||||||
public static async Task<int> RunAsync(string[] args)
|
public static async Task<int> RunAsync(string[] args)
|
||||||
{
|
{
|
||||||
if (args.Length != 4 || args[0] != "--mode" || args[1] != "PackageLifecyclePilot" || args[2] != "--root") return 2;
|
if (args.Length != 4 || args[0] != "--mode" || args[1] != "PackageLifecyclePilot" || args[2] != "--root") return 2;
|
||||||
ResolvedPackagePilotRoot pilotRoot; try { pilotRoot=new PathResolver().ResolvePackagePilotRoot(args[3]); } catch { return 2; }
|
ResolvedPackageLifecycleRoots roots; try { roots=new PathResolver().ResolvePackageLifecycleRoots(args[3]); } catch { return 2; }
|
||||||
var root=pilotRoot.Value; var id=Guid.NewGuid().ToString("N"); var evidenceDir=Path.Combine(root,"evidence",id); Directory.CreateDirectory(evidenceDir);
|
var id=Guid.NewGuid().ToString("N"); var started=DateTimeOffset.UtcNow; var evidenceDir=Path.Combine(roots.Evidence.Value,id);
|
||||||
|
Directory.CreateDirectory(evidenceDir); Directory.CreateDirectory(roots.Catalog.Value); Directory.CreateDirectory(Path.Combine(roots.Pilot.Value,"foreign-staging-fixture"));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var catalog=Path.Combine(root,"catalog"); Directory.CreateDirectory(catalog); var store=new LocalPackageStore(root,new Sha256FileHasher());
|
var store=new LocalPackageStore(roots.Pilot.Value,new Sha256FileHasher());
|
||||||
async Task<(ImmutableLocalPackage,PackageManifest)> Materialize(string version,byte seed)
|
async Task<(ImmutableLocalPackage Package,PackageManifest Manifest)> Materialize(string version,byte seed,LocalPackageStore target)
|
||||||
{ var a=new[]{seed,(byte)(seed+1)}; var b=new[]{(byte)(seed+2)}; await File.WriteAllBytesAsync(Path.Combine(catalog,"entry.dll"),a); await File.WriteAllBytesAsync(Path.Combine(catalog,"payload.dat"),b); var manifest=new PackageManifest(1,"Pilot.Package",version,"Plant3D",2026,"x64","entry.dll",[new("entry.dll",a.Length,Convert.ToHexString(SHA256.HashData(a))),new("payload.dat",b.Length,Convert.ToHexString(SHA256.HashData(b)))],new("none",""),RolloutChannel:"pilot"); return (await store.MaterializeAsync(manifest,catalog,CancellationToken.None),manifest); }
|
{ var a=new[]{seed,(byte)(seed+1)};var b=new[]{(byte)(seed+2)};await File.WriteAllBytesAsync(Path.Combine(roots.Catalog.Value,"entry.dll"),a);await File.WriteAllBytesAsync(Path.Combine(roots.Catalog.Value,"payload.dat"),b);var manifest=new PackageManifest(1,"Pilot.Package",version,"Plant3D",2026,"x64","entry.dll",[new("entry.dll",a.Length,Convert.ToHexString(SHA256.HashData(a))),new("payload.dat",b.Length,Convert.ToHexString(SHA256.HashData(b)))],new("none",""),RolloutChannel:"pilot");return(await target.MaterializeAsync(manifest,roots.Catalog.Value,CancellationToken.None),manifest); }
|
||||||
PackageRequirement Req(string v)=>new("Pilot.Package","Plant3D",2026,"x64","pilot",v,"pilot-roster",true);
|
PackageRequirement Req(string v)=>new("Pilot.Package","Plant3D",2026,"x64","pilot",v,"pilot-roster",true);
|
||||||
var (a,ma)=await Materialize("1.0.0",1); var pointersAfterA=await store.GetCurrentVersionAsync("Pilot.Package",CancellationToken.None); var aa=await store.ActivateImmutableAsync(a,Req("1.0.0"),CancellationToken.None); var repeat=await store.ActivateImmutableAsync(a,Req("1.0.0"),CancellationToken.None);
|
var initial=await store.GetCurrentVersionAsync("Pilot.Package",CancellationToken.None);
|
||||||
var (b,mb)=await Materialize("2.0.0",4); var beforeB=await store.GetCurrentVersionAsync("Pilot.Package",CancellationToken.None); var ab=await store.ActivateImmutableAsync(b,Req("2.0.0"),CancellationToken.None); var rollback=await store.RollbackAsync(ab.Receipt!,CancellationToken.None); var current=await store.GetCurrentVersionAsync("Pilot.Package",CancellationToken.None);
|
var (a,ma)=await Materialize("1.0.0",1,store); var afterMaterializeA=await store.GetCurrentVersionAsync("Pilot.Package",CancellationToken.None); await store.BindAsync(Req("1.0.0"),"1.0.0",CancellationToken.None);
|
||||||
var evidence=new { schemaVersion=1,mode="PackageLifecyclePilot",operationId=id,terminalState=current=="1.0.0"?"Succeeded":"Blocked",sourceClassification="SyntheticLocal",rootFingerprint=Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(id+root))),packages=new[]{new{version=ma.Version,manifestSha256=Convert.ToHexString(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(ma)))},new{version=mb.Version,manifestSha256=Convert.ToHexString(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(mb)))}},pointers=new{afterMaterializeA=pointersAfterA,afterActivateA=aa.Outcome,repeatedA=repeat.Outcome,beforeActivateB=beforeB,afterActivateB=ab.Outcome,afterRollback=rollback.Outcome,current},sideEffects=new{corporateDfsUsed=false,corporateDfsWriteAttempted=false,autoCadLaunchAttempted=false,processLaunchAttempted=false,productionPointerMutationAttempted=false,productionProjectAccessed=false}};
|
var aa=await store.ActivateImmutableAsync(a,Req("1.0.0"),CancellationToken.None); var repeat=await store.ActivateImmutableAsync(a,Req("1.0.0"),CancellationToken.None);
|
||||||
var final=Path.Combine(evidenceDir,"package-lifecycle-pilot.json"); var temp=final+".tmp"; await File.WriteAllTextAsync(temp,JsonSerializer.Serialize(evidence)); File.Move(temp,final,true); return current=="1.0.0"?0:2;
|
var (b,mb)=await Materialize("2.0.0",4,store); var beforeB=await store.GetCurrentVersionAsync("Pilot.Package",CancellationToken.None); await store.BindAsync(Req("2.0.0"),"2.0.0",CancellationToken.None);
|
||||||
|
var ab=await store.ActivateImmutableAsync(b,Req("2.0.0"),CancellationToken.None); var rollback=await store.RollbackAsync(ab.Receipt!,CancellationToken.None); var current=await store.GetCurrentVersionAsync("Pilot.Package",CancellationToken.None); var lkg=await store.ResolveAsync(Req("1.0.0"),CancellationToken.None);
|
||||||
|
|
||||||
|
async Task<string> ExpectedFault(PackageLifecycleFaultPoint point,string version)
|
||||||
|
{ try { await Materialize(version,9,new LocalPackageStore(roots.Pilot.Value,new Sha256FileHasher(),new Fault(point))); return "UnexpectedSuccess"; } catch { return "Blocked"; } }
|
||||||
|
var materializationFault=await ExpectedFault(PackageLifecycleFaultPoint.AfterPayloadCopyBeforeValidation,"9.0.0");
|
||||||
|
string pointerFault; try { await new LocalPackageStore(roots.Pilot.Value,new Sha256FileHasher(),new Fault(PackageLifecycleFaultPoint.BeforePointerReplace)).ActivateImmutableAsync(b,Req("2.0.0"),CancellationToken.None);pointerFault="UnexpectedSuccess";}catch{pointerFault="Blocked";}
|
||||||
|
var foreign=Path.Combine(roots.Pilot.Value,"foreign-staging-fixture");var fake=new PackageStagingOwnership("foreign","Pilot.Package","0",foreign,DateTimeOffset.UtcNow,"foreign");var cleanup=(await store.CleanupOwnedStagingAsync(fake,CancellationToken.None)).Outcome;
|
||||||
|
var success=initial is null&&afterMaterializeA is null&&aa.Outcome=="Activated"&&repeat.Outcome=="AlreadyActiveExact"&&beforeB=="1.0.0"&&ab.Outcome=="Activated"&&rollback.Outcome=="RolledBack"&¤t=="1.0.0"&&lkg?.Version=="1.0.0"&&materializationFault=="Blocked"&&pointerFault=="Blocked"&&cleanup=="OwnershipUnknown"&&Directory.Exists(foreign);
|
||||||
|
object Package(PackageManifest m)=>new{exactIdentity=new{m.PackageId,m.Product,m.ProductYear,m.Architecture,m.RolloutChannel,m.Version},manifestSha256=Convert.ToHexString(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(m))),payloadSetSha256=PayloadSet(m),payloadFiles=m.Files.Select(x=>new{x.Path,x.Length,x.Sha256})};
|
||||||
|
var evidence=new { schemaVersion=2,launcherCommit=ThisCommit(),operationId=id,mode="PackageLifecyclePilot",startedUtc=started,endedUtc=DateTimeOffset.UtcNow,terminalState=success?"Succeeded":"Blocked",exitClassification=success?"CONTROLLED_FILESYSTEM_PILOT_VERIFIED":"BLOCKED",sourceClassification="SyntheticLocal",typedRoots=new[]{Role("Catalog",roots.Catalog.Value,id),Role("Staging",roots.Staging.Value,id),Role("VersionStore",roots.VersionStore.Value,id),Role("Active",roots.Active.Value,id),Role("Evidence",roots.Evidence.Value,id)},packages=new[]{Package(ma),Package(mb)},pointers=new{initial,afterMaterializeA,afterActivateA=aa.Outcome,generationA=aa.Receipt?.PointerGeneration,repeatedA=repeat.Outcome,beforeActivateB=beforeB,afterActivateB=ab.Outcome,generationB=ab.Receipt?.PointerGeneration,afterRollback=rollback.Outcome,current,contentBoundSchema=2},exactLkg=new{selected=lkg?.Version,directoryScanAttempted=false,exactBinding=lkg is not null},failureScenarios=new[]{new{faultPoint="AfterPayloadCopyBeforeValidation",actual=materializationFault,invariant=current=="1.0.0"},new{faultPoint="BeforePointerReplace",actual=pointerFault,invariant=(await store.GetCurrentVersionAsync("Pilot.Package",CancellationToken.None))=="1.0.0"},new{faultPoint="OwnershipMismatch",actual=cleanup,invariant=Directory.Exists(foreign)}},locking=new{scope="ProcessLocal",crossProcessSafety="NotVerified",networkShareSafety="NotSupported"},sideEffects=new{corporateDfsUsed=false,corporateDfsWriteAttempted=false,autoCadLaunchAttempted=false,processLaunchAttempted=false,productionPointerMutationAttempted=false,productionProjectAccessed=false,foundationUiCreated=false},claims=new{materializationIsolation="Verified",ownedStagingCleanup="Verified",immutableContentBinding="Verified",activation="Verified",activationIdempotency="Verified",receiptBoundRollback="Verified",deterministicFailureRecovery="Verified",exactLkg="Verified",processLocalConcurrency="Verified",corporateDfs="NotVerified",autoCadRuntime="NotVerified",productionAcceptance="NotVerified"}};
|
||||||
|
var final=Path.Combine(evidenceDir,"package-lifecycle-pilot.json");var temp=final+"."+id+".tmp";await using(var stream=new FileStream(temp,FileMode.CreateNew,FileAccess.Write,FileShare.None)){await JsonSerializer.SerializeAsync(stream,evidence);await stream.FlushAsync();stream.Flush(true);}File.Move(temp,final);using var reread=JsonDocument.Parse(await File.ReadAllTextAsync(final));if(reread.RootElement.GetProperty("schemaVersion").GetInt32()!=2)return 10;return success?0:10;
|
||||||
}
|
}
|
||||||
catch { return 10; }
|
catch { return 10; }
|
||||||
}
|
}
|
||||||
|
private static object Role(string role,string path,string salt)=>new{role,localOnly=true,topologyValidated=true,reparseRejected=true,fingerprint=Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(salt+Path.GetFullPath(path))))};
|
||||||
|
private static string PayloadSet(PackageManifest m)=>Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("|",m.Files.OrderBy(x=>x.Path,StringComparer.Ordinal).Select(x=>$"{x.Path}:{x.Length}:{x.Sha256}")))));
|
||||||
|
private static string ThisCommit()=>Environment.GetEnvironmentVariable("LAUNCHER_COMMIT")??"WORKTREE_BUILD";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ public sealed record ValidationResult(IReadOnlyList<ValidationIssue> Issues)
|
|||||||
{ public bool IsValid => Issues.Count == 0; public static ValidationResult Success { get; } = new([]); }
|
{ public bool IsValid => Issues.Count == 0; public static ValidationResult Success { get; } = new([]); }
|
||||||
public sealed record PackageProgress(string Stage, int Completed, int Total);
|
public sealed record PackageProgress(string Stage, int Completed, int Total);
|
||||||
public sealed record ActivationResult(string CurrentVersion, string? PreviousVersion);
|
public sealed record ActivationResult(string CurrentVersion, string? PreviousVersion);
|
||||||
public sealed record PackageActivationReceipt(string OperationId, string PackageId, string Version, string? PreviousVersion, DateTimeOffset ActivatedUtc, long PointerGeneration = 0, string? ActiveRootIdentity = null, int SchemaVersion = 1, string? ManifestSha256 = null, string? PayloadSetSha256 = null, string? RosterIdentity = null);
|
public sealed record PackagePayloadBinding(string RelativePath,long Length,string Sha256);
|
||||||
|
public sealed record PackageActivationReceipt(string OperationId, string PackageId, string Version, string? PreviousVersion, DateTimeOffset ActivatedUtc, long PointerGeneration = 0, string? ActiveRootIdentity = null, int SchemaVersion = 2, string? ManifestSha256 = null, string? PayloadSetSha256 = null, string? RosterIdentity = null, IReadOnlyList<PackagePayloadBinding>? PayloadFiles = null, long PointerGenerationBefore = 0, string? EvidenceCorrelationId = null);
|
||||||
public sealed record PackageActivationResult(string Outcome, PackageActivationReceipt? Receipt);
|
public sealed record PackageActivationResult(string Outcome, PackageActivationReceipt? Receipt);
|
||||||
public sealed record PackageRollbackResult(string Outcome, string? CurrentVersion);
|
public sealed record PackageRollbackResult(string Outcome, string? CurrentVersion);
|
||||||
public sealed record ImmutableLocalPackage(string PackageId, string Version, string LocalPackageRoot);
|
public sealed record ImmutableLocalPackage(string PackageId, string Version, string LocalPackageRoot);
|
||||||
public sealed record PackageStagingOwnership(string OperationId, string PackageId, string Version, string StagingDirectory, DateTimeOffset CreatedUtc, string MarkerIdentity);
|
public sealed record PackageStagingOwnership(string OperationId, string PackageId, string Version, string StagingDirectory, DateTimeOffset CreatedUtc, string MarkerIdentity);
|
||||||
public sealed record PackageStagingCleanupResult(string Outcome, string OperationId);
|
public sealed record PackageStagingCleanupResult(string Outcome, string OperationId);
|
||||||
public enum PackageLifecycleFaultPoint { AfterStagingAllocated, DuringPayloadCopy, BeforeImmutableCommit, BeforePointerTempWrite, BeforePointerReplace, BeforePointerReadback, BeforeRollbackReplace, DuringOwnedStagingCleanup }
|
public enum PackageLifecycleFaultPoint { AfterStagingAllocated, DuringPayloadCopy, AfterPayloadCopyBeforeValidation, BeforeImmutableCommit, AfterImmutableCommitBeforeReturn, BeforePointerTempWrite, BeforePointerReplace, BeforePointerReadback, BeforeRollbackReplace, BeforeRollbackReadback, DuringOwnedStagingCleanup }
|
||||||
public interface IPackageLifecycleFaultInjector { void ThrowIfRequested(PackageLifecycleFaultPoint point); }
|
public interface IPackageLifecycleFaultInjector { void ThrowIfRequested(PackageLifecycleFaultPoint point); }
|
||||||
public sealed class NoOpPackageLifecycleFaultInjector : IPackageLifecycleFaultInjector { public void ThrowIfRequested(PackageLifecycleFaultPoint point) { } }
|
public sealed class NoOpPackageLifecycleFaultInjector : IPackageLifecycleFaultInjector { public void ThrowIfRequested(PackageLifecycleFaultPoint point) { } }
|
||||||
public interface IPackageMaterializer { Task<ImmutableLocalPackage> MaterializeAsync(PackageManifest manifest, string sourceRoot, CancellationToken cancellationToken, IProgress<PackageProgress>? progress = null); }
|
public interface IPackageMaterializer { Task<ImmutableLocalPackage> MaterializeAsync(PackageManifest manifest, string sourceRoot, CancellationToken cancellationToken, IProgress<PackageProgress>? progress = null); }
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ public sealed record ResolvedPackageStagingRoot { internal ResolvedPackageStagin
|
|||||||
public sealed record ResolvedActivePackageRoot { internal ResolvedActivePackageRoot(string value)=>Value=value; public string Value{get;} }
|
public sealed record ResolvedActivePackageRoot { internal ResolvedActivePackageRoot(string value)=>Value=value; public string Value{get;} }
|
||||||
public sealed record ResolvedPackageLifecycleEvidenceRoot { internal ResolvedPackageLifecycleEvidenceRoot(string value)=>Value=value; public string Value{get;} }
|
public sealed record ResolvedPackageLifecycleEvidenceRoot { internal ResolvedPackageLifecycleEvidenceRoot(string value)=>Value=value; public string Value{get;} }
|
||||||
public sealed record ResolvedPackagePilotRoot { internal ResolvedPackagePilotRoot(string value)=>Value=value; public string Value{get;} }
|
public sealed record ResolvedPackagePilotRoot { internal ResolvedPackagePilotRoot(string value)=>Value=value; public string Value{get;} }
|
||||||
|
public sealed record ResolvedPackageCatalogRoot { internal ResolvedPackageCatalogRoot(string value)=>Value=value; public string Value{get;} }
|
||||||
|
public sealed record ResolvedImmutablePackagePath { internal ResolvedImmutablePackagePath(string value)=>Value=value; public string Value{get;} }
|
||||||
|
public sealed record ResolvedPackageLifecycleRoots(
|
||||||
|
ResolvedPackagePilotRoot Pilot,
|
||||||
|
ResolvedPackageCatalogRoot Catalog,
|
||||||
|
ResolvedPackageStagingRoot Staging,
|
||||||
|
ResolvedPackageVersionStoreRoot VersionStore,
|
||||||
|
ResolvedActivePackageRoot Active,
|
||||||
|
ResolvedPackageLifecycleEvidenceRoot Evidence);
|
||||||
|
|
||||||
public interface IPathResolver
|
public interface IPathResolver
|
||||||
{
|
{
|
||||||
@@ -48,6 +57,7 @@ public interface IPathResolver
|
|||||||
ResolvedLegacyCompatibilityOverlayPath ResolveLegacyCompatibilityOverlayPath(LauncherPathVariables variables);
|
ResolvedLegacyCompatibilityOverlayPath ResolveLegacyCompatibilityOverlayPath(LauncherPathVariables variables);
|
||||||
ResolvedLegacyProbeEvidenceRoot ResolveLegacyProbeEvidenceRoot(LauncherPathVariables variables);
|
ResolvedLegacyProbeEvidenceRoot ResolveLegacyProbeEvidenceRoot(LauncherPathVariables variables);
|
||||||
ResolvedPackagePilotRoot ResolvePackagePilotRoot(string value);
|
ResolvedPackagePilotRoot ResolvePackagePilotRoot(string value);
|
||||||
|
ResolvedPackageLifecycleRoots ResolvePackageLifecycleRoots(string value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environment = null) : IPathResolver
|
public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environment = null) : IPathResolver
|
||||||
@@ -117,7 +127,15 @@ public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environme
|
|||||||
return new(value);
|
return new(value);
|
||||||
}
|
}
|
||||||
public ResolvedPackagePilotRoot ResolvePackagePilotRoot(string value)
|
public ResolvedPackagePilotRoot ResolvePackagePilotRoot(string value)
|
||||||
{ if (string.IsNullOrWhiteSpace(value)||value.Contains('*')||value.Contains('?')||value.StartsWith(@"\\")||!Path.IsPathFullyQualified(value)) throw new InvalidOperationException("PACKAGE_PILOT_ROOT_NOT_LOCAL"); var full=Path.GetFullPath(value); var info=new DirectoryInfo(full); if(info.Exists&&info.Attributes.HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("PACKAGE_ROOT_REPARSE_REJECTED"); return new(full); }
|
{ if (string.IsNullOrWhiteSpace(value)||Uri.IsWellFormedUriString(value,UriKind.Absolute)||value.Contains('*')||value.Contains('?')||value.StartsWith(@"\\")||!Path.IsPathFullyQualified(value)) throw new InvalidOperationException("PACKAGE_PILOT_ROOT_NOT_LOCAL"); var full=Path.GetFullPath(value); var info=new DirectoryInfo(full); if(info.Exists&&info.Attributes.HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("PACKAGE_ROOT_REPARSE_REJECTED"); return new(full); }
|
||||||
|
public ResolvedPackageLifecycleRoots ResolvePackageLifecycleRoots(string value)
|
||||||
|
{
|
||||||
|
var pilot=ResolvePackagePilotRoot(value); string Child(string name)=>Path.GetFullPath(Path.Combine(pilot.Value,name));
|
||||||
|
var paths=new[]{Child("catalog"),Child("staging"),Child("version-store"),Child("active"),Child("evidence")};
|
||||||
|
if(paths.Distinct(StringComparer.OrdinalIgnoreCase).Count()!=paths.Length) throw new InvalidOperationException("PACKAGE_ROOT_COLLISION");
|
||||||
|
foreach(var path in paths) if(!path.StartsWith(pilot.Value.TrimEnd(Path.DirectorySeparatorChar)+Path.DirectorySeparatorChar,StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("PACKAGE_ROOT_TOPOLOGY_INVALID");
|
||||||
|
return new(pilot,new(paths[0]),new(paths[1]),new(paths[2]),new(paths[3]),new(paths[4]));
|
||||||
|
}
|
||||||
private string ResolveExistingFile(string role, LauncherPathVariables variables)
|
private string ResolveExistingFile(string role, LauncherPathVariables variables)
|
||||||
{
|
{
|
||||||
var value=ResolvePath(Resolve(variables,[role]).Require(role),variables);
|
var value=ResolvePath(Resolve(variables,[role]).Require(role),variables);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher, IPackageL
|
|||||||
{
|
{
|
||||||
// Machine-process local serialization only; package roots must remain local.
|
// Machine-process local serialization only; package roots must remain local.
|
||||||
private static readonly SemaphoreSlim PointerGate = new(1, 1);
|
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, string? ManifestSha256 = null, string? PayloadSetSha256 = null);
|
private sealed record Pointer(string Current, string? Previous, int SchemaVersion = 2, long Generation = 0, string? ActivationOperationId = null, DateTimeOffset? ActivatedAtUtc = null, string? ManifestSha256 = null, string? PayloadSetSha256 = null, IReadOnlyList<PackagePayloadBinding>? PayloadFiles = null);
|
||||||
private sealed record CacheBinding(string PackageId, string Product, int ProductYear, string Architecture, string Channel, string RequiredVersion, string RosterIdentity);
|
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 StoredManifestFileName = "package.manifest.json";
|
||||||
private const string StoredBindingFileName = "package.binding.json";
|
private const string StoredBindingFileName = "package.binding.json";
|
||||||
@@ -20,7 +20,7 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher, IPackageL
|
|||||||
if (!File.Exists(PointerPath(id))) return null;
|
if (!File.Exists(PointerPath(id))) return null;
|
||||||
await using var stream = File.OpenRead(PointerPath(id));
|
await using var stream = File.OpenRead(PointerPath(id));
|
||||||
var pointer = 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;
|
return pointer is null || pointer.SchemaVersion is < 1 or > 2 || string.IsNullOrWhiteSpace(pointer.Current) ? null : pointer;
|
||||||
}
|
}
|
||||||
public async Task<string?> GetCurrentVersionAsync(string id, CancellationToken ct) => (await ReadPointer(id, ct))?.Current;
|
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<string?> GetPreviousVersionAsync(string id, CancellationToken ct) => (await ReadPointer(id, ct))?.Previous;
|
||||||
@@ -30,7 +30,7 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher, IPackageL
|
|||||||
{
|
{
|
||||||
var dir = VersionDirectory(id, version); if (!Directory.Exists(dir)) throw new DirectoryNotFoundException(dir);
|
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 content=await ContentFingerprintAsync(id,version,ct);
|
Directory.CreateDirectory(PackageRoot(id)); var prior = await ReadPointer(id, ct); var operationId=Guid.NewGuid().ToString("N"); var content=await ContentFingerprintAsync(id,version,ct);
|
||||||
(faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforePointerTempWrite); 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,content.Manifest,content.PayloadSet),cancellationToken:ct); await stream.FlushAsync(ct); }
|
(faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforePointerTempWrite); 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,2,(prior?.Generation??0)+1,operationId,DateTimeOffset.UtcNow,content.Manifest,content.PayloadSet,content.Files),cancellationToken:ct); await stream.FlushAsync(ct); }
|
||||||
(faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforePointerReplace); File.Move(temp, PointerPath(id), true); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforePointerReadback); var verified=await ReadPointer(id,ct); if (verified?.Current!=version || verified.ActivationOperationId!=operationId) throw new InvalidOperationException("POINTER_REVALIDATION_FAILED");
|
(faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforePointerReplace); File.Move(temp, PointerPath(id), true); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforePointerReadback); var verified=await ReadPointer(id,ct); if (verified?.Current!=version || verified.ActivationOperationId!=operationId) throw new InvalidOperationException("POINTER_REVALIDATION_FAILED");
|
||||||
return new(version, prior?.Current);
|
return new(version, prior?.Current);
|
||||||
} finally { PointerGate.Release(); }
|
} finally { PointerGate.Release(); }
|
||||||
@@ -39,16 +39,16 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher, IPackageL
|
|||||||
{
|
{
|
||||||
if (!string.Equals(package.PackageId, requirement.PackageId, StringComparison.Ordinal) || !string.Equals(package.Version, requirement.RequiredVersion, StringComparison.Ordinal) || !await MatchesImmutableManifestAsync(package,requirement,ct)) return new("Blocked", null);
|
if (!string.Equals(package.PackageId, requirement.PackageId, StringComparison.Ordinal) || !string.Equals(package.Version, requirement.RequiredVersion, StringComparison.Ordinal) || !await MatchesImmutableManifestAsync(package,requirement,ct)) return new("Blocked", null);
|
||||||
var prior = await ReadPointer(package.PackageId, ct);
|
var prior = await ReadPointer(package.PackageId, ct);
|
||||||
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),1,prior.ManifestSha256,prior.PayloadSetSha256,requirement.RosterIdentity));
|
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),prior.SchemaVersion,prior.ManifestSha256,prior.PayloadSetSha256,requirement.RosterIdentity,prior.PayloadFiles,Math.Max(0,prior.Generation-1),prior.ActivationOperationId));
|
||||||
var activated = await ActivateAsync(package.PackageId, package.Version, ct);
|
var activated = await ActivateAsync(package.PackageId, package.Version, ct);
|
||||||
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),1,after.ManifestSha256,after.PayloadSetSha256,requirement.RosterIdentity));
|
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),after.SchemaVersion,after.ManifestSha256,after.PayloadSetSha256,requirement.RosterIdentity,after.PayloadFiles,Math.Max(0,after.Generation-1),after.ActivationOperationId));
|
||||||
}
|
}
|
||||||
public async Task<PackageRollbackResult> RollbackAsync(PackageActivationReceipt receipt, CancellationToken ct)
|
public async Task<PackageRollbackResult> RollbackAsync(PackageActivationReceipt receipt, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var current=await GetCurrentVersionAsync(receipt.PackageId,ct);
|
var current=await GetCurrentVersionAsync(receipt.PackageId,ct);
|
||||||
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);
|
var pointer=await ReadPointer(receipt.PackageId,ct); if (receipt.SchemaVersion!=2 || pointer is null || !string.Equals(current,receipt.Version,StringComparison.Ordinal) || pointer.ActivationOperationId!=receipt.OperationId || pointer.Generation!=receipt.PointerGeneration || !string.Equals(pointer.ManifestSha256,receipt.ManifestSha256,StringComparison.OrdinalIgnoreCase) || !string.Equals(pointer.PayloadSetSha256,receipt.PayloadSetSha256,StringComparison.OrdinalIgnoreCase) || !string.Equals(receipt.ActiveRootIdentity,PackageRoot(receipt.PackageId),StringComparison.OrdinalIgnoreCase)) return new("RollbackBlockedStateChanged",current);
|
||||||
if (string.IsNullOrWhiteSpace(receipt.PreviousVersion)) return new("RollbackNotApplicable",current);
|
if (string.IsNullOrWhiteSpace(receipt.PreviousVersion)) return new("RollbackNotApplicable",current);
|
||||||
var restored=await ActivateAsync(receipt.PackageId,receipt.PreviousVersion,ct);
|
(faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforeRollbackReplace); var restored=await ActivateAsync(receipt.PackageId,receipt.PreviousVersion,ct); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforeRollbackReadback);
|
||||||
return new("RolledBack",restored.CurrentVersion);
|
return new("RolledBack",restored.CurrentVersion);
|
||||||
}
|
}
|
||||||
public async Task<string?> RollbackAsync(string id, CancellationToken ct) { var p = await ReadPointer(id, ct); return p?.Previous is null ? null : (await ActivateAsync(id, p.Previous, ct)).CurrentVersion; }
|
public async Task<string?> RollbackAsync(string id, CancellationToken ct) { var p = await ReadPointer(id, ct); return p?.Previous is null ? null : (await ActivateAsync(id, p.Previous, ct)).CurrentVersion; }
|
||||||
@@ -63,6 +63,7 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher, IPackageL
|
|||||||
var pointer = await ReadPointer(requirement.PackageId, ct);
|
var pointer = await ReadPointer(requirement.PackageId, ct);
|
||||||
if (pointer is null) return null;
|
if (pointer is null) return null;
|
||||||
var current = await VerifyVersionAsync(requirement, pointer.Current, ct);
|
var current = await VerifyVersionAsync(requirement, pointer.Current, ct);
|
||||||
|
if(current is not null && !await PointerMatchesContentAsync(pointer,requirement.PackageId,pointer.Current,ct)) current=null;
|
||||||
if (current is not null) return new(requirement.PackageId, pointer.Current, current, false, "CURRENT_VERIFIED");
|
if (current is not null) return new(requirement.PackageId, pointer.Current, current, false, "CURRENT_VERIFIED");
|
||||||
if (!requirement.AllowOfflineFallback || string.IsNullOrWhiteSpace(pointer.Previous) || string.Equals(pointer.Current, pointer.Previous, StringComparison.OrdinalIgnoreCase)) return null;
|
if (!requirement.AllowOfflineFallback || string.IsNullOrWhiteSpace(pointer.Previous) || string.Equals(pointer.Current, pointer.Previous, StringComparison.OrdinalIgnoreCase)) return null;
|
||||||
var previous = await VerifyVersionAsync(requirement, pointer.Previous, ct);
|
var previous = await VerifyVersionAsync(requirement, pointer.Previous, ct);
|
||||||
@@ -79,9 +80,10 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher, IPackageL
|
|||||||
if (!new PackageManifestValidator().Validate(manifest, manifest.ProductYear, manifest.Architecture).IsValid) throw new InvalidOperationException("MANIFEST_INVALID");
|
if (!new PackageManifestValidator().Validate(manifest, manifest.ProductYear, manifest.Architecture).IsValid) throw new InvalidOperationException("MANIFEST_INVALID");
|
||||||
if (manifest.Files.Any(x => string.Equals(x.Path, StoredManifestFileName, StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException("MANIFEST_RESERVED_FILE");
|
if (manifest.Files.Any(x => string.Equals(x.Path, StoredManifestFileName, StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException("MANIFEST_RESERVED_FILE");
|
||||||
var operationId = Guid.NewGuid().ToString("N"); var staging = Path.Combine(root, "Launcher", "staging", operationId); Directory.CreateDirectory(staging); await File.WriteAllTextAsync(Path.Combine(staging, ".package-staging-owner.json"), JsonSerializer.Serialize(new PackageStagingOwnership(operationId, manifest.PackageId, manifest.Version, staging, DateTimeOffset.UtcNow, operationId)), ct); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.AfterStagingAllocated);
|
var operationId = Guid.NewGuid().ToString("N"); var staging = Path.Combine(root, "Launcher", "staging", operationId); Directory.CreateDirectory(staging); await File.WriteAllTextAsync(Path.Combine(staging, ".package-staging-owner.json"), JsonSerializer.Serialize(new PackageStagingOwnership(operationId, manifest.PackageId, manifest.Version, staging, DateTimeOffset.UtcNow, operationId)), ct); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.AfterStagingAllocated);
|
||||||
try { var i = 0; foreach (var f in manifest.Files) { ct.ThrowIfCancellationRequested(); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.DuringPayloadCopy); var source = Path.Combine(sourceRoot, f.Path); var dest = PackageFilePath(staging, f.Path); Directory.CreateDirectory(Path.GetDirectoryName(dest)!); File.Copy(source, dest, true); if (!string.Equals(await hasher.Sha256Async(dest, ct), f.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("HASH_MISMATCH"); progress?.Report(new("copy", ++i, manifest.Files.Count)); }
|
try { var i = 0; foreach (var f in manifest.Files) { ct.ThrowIfCancellationRequested(); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.DuringPayloadCopy); var source = PackageFilePath(sourceRoot, f.Path); var sourceInfo=new FileInfo(source); if(!sourceInfo.Exists||sourceInfo.Length!=f.Length||!string.Equals(await hasher.Sha256Async(source,ct),f.Sha256,StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("SOURCE_HASH_MISMATCH"); var dest = PackageFilePath(staging, f.Path); Directory.CreateDirectory(Path.GetDirectoryName(dest)!); File.Copy(source, dest, true); if (!string.Equals(await hasher.Sha256Async(dest, ct), f.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("HASH_MISMATCH"); progress?.Report(new("copy", ++i, manifest.Files.Count)); }
|
||||||
|
(faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.AfterPayloadCopyBeforeValidation);
|
||||||
await File.WriteAllTextAsync(Path.Combine(staging, StoredManifestFileName), JsonSerializer.Serialize(manifest), ct);
|
await File.WriteAllTextAsync(Path.Combine(staging, StoredManifestFileName), JsonSerializer.Serialize(manifest), ct);
|
||||||
File.Delete(Path.Combine(staging, ".package-staging-owner.json")); var versionDir = VersionDirectory(manifest.PackageId, manifest.Version); Directory.CreateDirectory(Path.GetDirectoryName(versionDir)!); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforeImmutableCommit); if (Directory.Exists(versionDir)) Directory.Delete(staging, true); else Directory.Move(staging, versionDir); return new(manifest.PackageId, manifest.Version, versionDir); }
|
File.Delete(Path.Combine(staging, ".package-staging-owner.json")); var versionDir = VersionDirectory(manifest.PackageId, manifest.Version); Directory.CreateDirectory(Path.GetDirectoryName(versionDir)!); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.BeforeImmutableCommit); if (Directory.Exists(versionDir)) { if(!await ExistingMatchesAsync(versionDir,manifest,ct)) throw new InvalidOperationException("IMMUTABLE_PACKAGE_CONFLICT"); Directory.Delete(staging,true); } else Directory.Move(staging, versionDir); (faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.AfterImmutableCommitBeforeReturn); return new(manifest.PackageId, manifest.Version, versionDir); }
|
||||||
catch { await CleanupOwnedStagingAsync(new PackageStagingOwnership(operationId, manifest.PackageId, manifest.Version, staging, DateTimeOffset.UtcNow, operationId), CancellationToken.None); throw; }
|
catch { await CleanupOwnedStagingAsync(new PackageStagingOwnership(operationId, manifest.PackageId, manifest.Version, staging, DateTimeOffset.UtcNow, operationId), CancellationToken.None); throw; }
|
||||||
}
|
}
|
||||||
public async Task<PackageStagingCleanupResult> CleanupOwnedStagingAsync(PackageStagingOwnership ownership, CancellationToken ct)
|
public async Task<PackageStagingCleanupResult> CleanupOwnedStagingAsync(PackageStagingOwnership ownership, CancellationToken ct)
|
||||||
@@ -91,7 +93,7 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher, IPackageL
|
|||||||
if (!Directory.Exists(directory)) return new("AlreadyAbsent",ownership.OperationId);
|
if (!Directory.Exists(directory)) return new("AlreadyAbsent",ownership.OperationId);
|
||||||
var marker=Path.Combine(directory,".package-staging-owner.json"); if (!File.Exists(marker)) return new("OwnershipUnknown",ownership.OperationId);
|
var marker=Path.Combine(directory,".package-staging-owner.json"); if (!File.Exists(marker)) return new("OwnershipUnknown",ownership.OperationId);
|
||||||
var stored=JsonSerializer.Deserialize<PackageStagingOwnership>(await File.ReadAllTextAsync(marker,ct)); if (stored is null || stored.OperationId!=ownership.OperationId || stored.PackageId!=ownership.PackageId || stored.Version!=ownership.Version) return new("OwnershipMismatch",ownership.OperationId);
|
var stored=JsonSerializer.Deserialize<PackageStagingOwnership>(await File.ReadAllTextAsync(marker,ct)); if (stored is null || stored.OperationId!=ownership.OperationId || stored.PackageId!=ownership.PackageId || stored.Version!=ownership.Version) return new("OwnershipMismatch",ownership.OperationId);
|
||||||
Directory.Delete(directory,true); return new("Cleaned",ownership.OperationId);
|
(faults??new NoOpPackageLifecycleFaultInjector()).ThrowIfRequested(PackageLifecycleFaultPoint.DuringOwnedStagingCleanup); Directory.Delete(directory,true); return new("Cleaned",ownership.OperationId);
|
||||||
}
|
}
|
||||||
private async Task<string?> VerifyVersionAsync(PackageRequirement requirement, string version, CancellationToken ct)
|
private async Task<string?> VerifyVersionAsync(PackageRequirement requirement, string version, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -114,8 +116,12 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher, IPackageL
|
|||||||
}
|
}
|
||||||
return versionRoot;
|
return versionRoot;
|
||||||
}
|
}
|
||||||
private async Task<(string Manifest,string PayloadSet)> ContentFingerprintAsync(string id,string version,CancellationToken ct)
|
private async Task<(string Manifest,string PayloadSet,IReadOnlyList<PackagePayloadBinding> Files)> ContentFingerprintAsync(string id,string version,CancellationToken ct)
|
||||||
{ var dir=VersionDirectory(id,version); var manifestPath=Path.Combine(dir,StoredManifestFileName); var manifest=PackageManifestJson.Parse(await File.ReadAllTextAsync(manifestPath,ct)); var manifestHash=await hasher.Sha256Async(manifestPath,ct); var payload=string.Join("|",manifest.Files.OrderBy(x=>x.Path,StringComparer.Ordinal).Select(x=>$"{x.Path}:{x.Length}:{x.Sha256}")); return (manifestHash,Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(payload)))); }
|
{ var dir=VersionDirectory(id,version); var manifestPath=Path.Combine(dir,StoredManifestFileName); var manifest=PackageManifestJson.Parse(await File.ReadAllTextAsync(manifestPath,ct)); var manifestHash=await hasher.Sha256Async(manifestPath,ct); var files=manifest.Files.OrderBy(x=>x.Path,StringComparer.Ordinal).Select(x=>new PackagePayloadBinding(x.Path.Replace('\\','/'),x.Length,x.Sha256.ToUpperInvariant())).ToArray(); var payload=string.Join("|",files.Select(x=>$"{x.RelativePath}:{x.Length}:{x.Sha256}")); return (manifestHash,Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(payload))),files); }
|
||||||
|
private async Task<bool> PointerMatchesContentAsync(Pointer pointer,string id,string version,CancellationToken ct)
|
||||||
|
{ if(pointer.SchemaVersion==1) return true; try { var content=await ContentFingerprintAsync(id,version,ct); return string.Equals(pointer.ManifestSha256,content.Manifest,StringComparison.OrdinalIgnoreCase)&&string.Equals(pointer.PayloadSetSha256,content.PayloadSet,StringComparison.OrdinalIgnoreCase)&&pointer.PayloadFiles is not null&&pointer.PayloadFiles.SequenceEqual(content.Files); } catch{return false;} }
|
||||||
|
private async Task<bool> ExistingMatchesAsync(string directory,PackageManifest expected,CancellationToken ct)
|
||||||
|
{ try { var stored=PackageManifestJson.Parse(await File.ReadAllTextAsync(Path.Combine(directory,StoredManifestFileName),ct)); if(stored!=expected) return false; foreach(var file in expected.Files){var path=PackageFilePath(directory,file.Path);var info=new FileInfo(path);if(!info.Exists||info.Length!=file.Length||!string.Equals(await hasher.Sha256Async(path,ct),file.Sha256,StringComparison.OrdinalIgnoreCase))return false;}return true;}catch{return false;} }
|
||||||
private async Task<bool> MatchesImmutableManifestAsync(ImmutableLocalPackage package,PackageRequirement requirement,CancellationToken ct)
|
private async Task<bool> MatchesImmutableManifestAsync(ImmutableLocalPackage package,PackageRequirement requirement,CancellationToken ct)
|
||||||
{ try { if (!package.LocalPackageRoot.Equals(VersionDirectory(package.PackageId,package.Version),StringComparison.OrdinalIgnoreCase)) return false; var manifest=PackageManifestJson.Parse(await File.ReadAllTextAsync(Path.Combine(package.LocalPackageRoot,StoredManifestFileName),ct)); if (!string.Equals(manifest.PackageId,requirement.PackageId,StringComparison.Ordinal)||!string.Equals(manifest.Version,requirement.RequiredVersion,StringComparison.Ordinal)||!string.Equals(manifest.Product,requirement.Product,StringComparison.Ordinal)||manifest.ProductYear!=requirement.ProductYear||!string.Equals(manifest.Architecture,requirement.Architecture,StringComparison.OrdinalIgnoreCase)||!string.Equals(manifest.RolloutChannel,requirement.Channel,StringComparison.OrdinalIgnoreCase)) return false; foreach(var f in manifest.Files){var p=PackageFilePath(package.LocalPackageRoot,f.Path); var i=new FileInfo(p); if(!i.Exists||i.Length!=f.Length||!string.Equals(await hasher.Sha256Async(p,ct),f.Sha256,StringComparison.OrdinalIgnoreCase)) return false;} return true; } catch { return false; } }
|
{ try { if (!package.LocalPackageRoot.Equals(VersionDirectory(package.PackageId,package.Version),StringComparison.OrdinalIgnoreCase)) return false; var manifest=PackageManifestJson.Parse(await File.ReadAllTextAsync(Path.Combine(package.LocalPackageRoot,StoredManifestFileName),ct)); if (!string.Equals(manifest.PackageId,requirement.PackageId,StringComparison.Ordinal)||!string.Equals(manifest.Version,requirement.RequiredVersion,StringComparison.Ordinal)||!string.Equals(manifest.Product,requirement.Product,StringComparison.Ordinal)||manifest.ProductYear!=requirement.ProductYear||!string.Equals(manifest.Architecture,requirement.Architecture,StringComparison.OrdinalIgnoreCase)||!string.Equals(manifest.RolloutChannel,requirement.Channel,StringComparison.OrdinalIgnoreCase)) return false; foreach(var f in manifest.Files){var p=PackageFilePath(package.LocalPackageRoot,f.Path); var i=new FileInfo(p); if(!i.Exists||i.Length!=f.Length||!string.Equals(await hasher.Sha256Async(p,ct),f.Sha256,StringComparison.OrdinalIgnoreCase)) return false;} return true; } catch { return false; } }
|
||||||
private static string PackageFilePath(string packageRoot, string relativePath)
|
private static string PackageFilePath(string packageRoot, string relativePath)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Package activation
|
# Package activation
|
||||||
|
|
||||||
Before changing package pointers, Launcher Next writes an activation snapshot containing exact previous `current.json` and `previous.json` contents and their SHA-256 values. Files are copied to a unique staging directory and rehashed. Only then is staging moved to `packages/<package>/versions/<version>` and the package-local pointer updated atomically.
|
Files are copied to an operation-owned staging directory, rehashed and moved to a deterministic immutable version location. Materialization never changes activation pointers. Explicit activation writes schema-v2 `current.json` through an operation-owned temporary file, flush, replace and readback. The pointer binds manifest SHA-256, deterministic payload-set SHA-256 and every payload relative path/length/SHA-256.
|
||||||
|
|
||||||
|
Activation and receipt-bound rollback are serialized by a process-local `SemaphoreSlim`. Cross-process coordination is not verified and network-share pointer mutation is not supported. The controlled lifecycle pilot uses only an explicit local root and does not imply corporate DFS, AutoCAD runtime or production acceptance.
|
||||||
|
|
||||||
Rollback restores the exact pointers, deletes the pilot version and startup script, and removes incomplete staging. Autodesk assemblies, PDBs, executables, test assemblies and legacy files are rejected from the pilot package.
|
Rollback restores the exact pointers, deletes the pilot version and startup script, and removes incomplete staging. Autodesk assemblies, PDBs, executables, test assemblies and legacy files are rejected from the pilot package.
|
||||||
|
|
||||||
@@ -10,14 +12,14 @@ Each activated version retains its validated package manifest alongside its payl
|
|||||||
`PackageResolutionUseCase` receives an explicit roster requirement: package ID,
|
`PackageResolutionUseCase` receives an explicit roster requirement: package ID,
|
||||||
product/year/architecture, channel, exact version, roster identity and offline
|
product/year/architecture, channel, exact version, roster identity and offline
|
||||||
policy. On an online acquisition it stages locally, binds that requirement to the
|
policy. On an online acquisition it stages locally, binds that requirement to the
|
||||||
activated version, then revalidates through `LocalPackageStore.ResolveAsync`.
|
immutable version, then revalidates through `LocalPackageStore.ResolveAsync`.
|
||||||
|
|
||||||
The resolver checks manifest, binding, identity/product compatibility, entry and
|
The resolver checks manifest, binding, identity/product compatibility, entry and
|
||||||
all payload lengths/SHA-256 before returning a local version. It returns current
|
all payload lengths/SHA-256 before returning a local version. It returns current
|
||||||
first. Only when current is invalid and the requirement explicitly permits offline
|
first. Only when current is invalid and the requirement explicitly permits offline
|
||||||
fallback can it return previous. A cache entry with another roster, channel or
|
fallback can it return previous. A cache entry with another roster, channel or
|
||||||
version is rejected. It never reads or executes a package from DFS, and a failed
|
version is rejected. It never reads or executes a package from DFS, and a failed
|
||||||
or corrupted update never becomes last-known-good.
|
or corrupted update never becomes last-known-good. Resolution follows only the explicit current/previous pointers and never enumerates the version store to find an approximate candidate.
|
||||||
|
|
||||||
Resolution emits stable evidence codes: `ONLINE_PACKAGE_SELECTED`,
|
Resolution emits stable evidence codes: `ONLINE_PACKAGE_SELECTED`,
|
||||||
`VALID_CURRENT_CACHE_SELECTED`, `PREVIOUS_LKG_SELECTED`,
|
`VALID_CURRENT_CACHE_SELECTED`, `PREVIOUS_LKG_SELECTED`,
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
"realCorporateRosterParsing": "BLOCKED",
|
"realCorporateRosterParsing": "BLOCKED",
|
||||||
"realCorporatePackageAcquisition": "BLOCKED",
|
"realCorporatePackageAcquisition": "BLOCKED",
|
||||||
"blockReason": "CORPORATE_CATALOG_AND_ROSTER_PATHS_NOT_CONFIGURED",
|
"blockReason": "CORPORATE_CATALOG_AND_ROSTER_PATHS_NOT_CONFIGURED",
|
||||||
"localImmutableStaging": "VERIFIED_BY_TESTS",
|
"localImmutableStaging": "CONTROLLED_FILESYSTEM_PILOT_VERIFIED",
|
||||||
"controlledOutageRecovery": "VERIFIED_BY_TESTS",
|
"localPackageLifecycle": "CONTROLLED_FILESYSTEM_PILOT_VERIFIED",
|
||||||
"productionActivation": "NOT_RUN",
|
"controlledOutageRecovery": "CONTROLLED_FILESYSTEM_PILOT_VERIFIED",
|
||||||
"autodeskRuntime": "NOT_RUN",
|
"crossProcessActivationCoordination": "NOT_RUNTIME_VERIFIED",
|
||||||
"productionReadiness": "NOT_ACHIEVED",
|
"productionActivation": "NOT_RUNTIME_VERIFIED",
|
||||||
|
"autodeskRuntime": "NOT_RUNTIME_VERIFIED",
|
||||||
|
"productionReadiness": "NOT_RUNTIME_VERIFIED",
|
||||||
"redaction": "No corporate paths, roster contents, package payload, session identity, or machine data were recorded."
|
"redaction": "No corporate paths, roster contents, package payload, session identity, or machine data were recorded."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json;
|
||||||
using Launcher.Next.Core;
|
using Launcher.Next.Core;
|
||||||
using Launcher.Next.Infrastructure;
|
using Launcher.Next.Infrastructure;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -12,6 +13,16 @@ public sealed class PackageLifecycleTests
|
|||||||
{ var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var source=Path.Combine(root,"catalog"); Directory.CreateDirectory(source); await File.WriteAllBytesAsync(Path.Combine(source,"entry.dll"),[1]); await File.WriteAllBytesAsync(Path.Combine(source,"payload.dat"),[2]); var store=new LocalPackageStore(root,new Sha256FileHasher(),new Fault(PackageLifecycleFaultPoint.AfterStagingAllocated)); try { await Assert.ThrowsAsync<IOException>(()=>store.MaterializeAsync(Manifest("1.0.0",[1],[2]),source,CancellationToken.None)); Assert.Null(await store.GetCurrentVersionAsync("Lifecycle.Package",CancellationToken.None)); } finally { if(Directory.Exists(root)) Directory.Delete(root,true); } }
|
{ var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var source=Path.Combine(root,"catalog"); Directory.CreateDirectory(source); await File.WriteAllBytesAsync(Path.Combine(source,"entry.dll"),[1]); await File.WriteAllBytesAsync(Path.Combine(source,"payload.dat"),[2]); var store=new LocalPackageStore(root,new Sha256FileHasher(),new Fault(PackageLifecycleFaultPoint.AfterStagingAllocated)); try { await Assert.ThrowsAsync<IOException>(()=>store.MaterializeAsync(Manifest("1.0.0",[1],[2]),source,CancellationToken.None)); Assert.Null(await store.GetCurrentVersionAsync("Lifecycle.Package",CancellationToken.None)); } finally { if(Directory.Exists(root)) Directory.Delete(root,true); } }
|
||||||
[Fact] public void PilotRootRejectsUncAndRelativePaths()
|
[Fact] public void PilotRootRejectsUncAndRelativePaths()
|
||||||
{ var resolver=new PathResolver(new Dictionary<string,string?>()); Assert.Throws<InvalidOperationException>(()=>resolver.ResolvePackagePilotRoot(@"\\server\share")); Assert.Throws<InvalidOperationException>(()=>resolver.ResolvePackagePilotRoot("relative")); }
|
{ var resolver=new PathResolver(new Dictionary<string,string?>()); Assert.Throws<InvalidOperationException>(()=>resolver.ResolvePackagePilotRoot(@"\\server\share")); Assert.Throws<InvalidOperationException>(()=>resolver.ResolvePackagePilotRoot("relative")); }
|
||||||
|
[Fact] public void LifecycleRootsAreTypedDistinctAndConfined()
|
||||||
|
{ var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var roots=new PathResolver(new Dictionary<string,string?>()).ResolvePackageLifecycleRoots(root); var values=new[]{roots.Catalog.Value,roots.Staging.Value,roots.VersionStore.Value,roots.Active.Value,roots.Evidence.Value}; Assert.Equal(values.Length,values.Distinct(StringComparer.OrdinalIgnoreCase).Count()); Assert.All(values,x=>Assert.StartsWith(roots.Pilot.Value,x,StringComparison.OrdinalIgnoreCase)); }
|
||||||
|
[Theory]
|
||||||
|
[InlineData(PackageLifecycleFaultPoint.AfterStagingAllocated)]
|
||||||
|
[InlineData(PackageLifecycleFaultPoint.DuringPayloadCopy)]
|
||||||
|
[InlineData(PackageLifecycleFaultPoint.AfterPayloadCopyBeforeValidation)]
|
||||||
|
[InlineData(PackageLifecycleFaultPoint.BeforeImmutableCommit)]
|
||||||
|
[InlineData(PackageLifecycleFaultPoint.AfterImmutableCommitBeforeReturn)]
|
||||||
|
public async Task DeterministicMaterializationFaultsNeverMutatePointers(PackageLifecycleFaultPoint point)
|
||||||
|
{ var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N"));var source=Path.Combine(root,"catalog");Directory.CreateDirectory(source);await File.WriteAllBytesAsync(Path.Combine(source,"entry.dll"),[1]);await File.WriteAllBytesAsync(Path.Combine(source,"payload.dat"),[2]);var store=new LocalPackageStore(root,new Sha256FileHasher(),new Fault(point));try{await Assert.ThrowsAnyAsync<Exception>(()=>store.MaterializeAsync(Manifest("1.0.0",[1],[2]),source,CancellationToken.None));Assert.Null(await store.GetCurrentVersionAsync("Lifecycle.Package",CancellationToken.None));Assert.Null(await store.GetPreviousVersionAsync("Lifecycle.Package",CancellationToken.None));}finally{if(Directory.Exists(root))Directory.Delete(root,true);} }
|
||||||
private static PackageRequirement Req(string version) => new("Lifecycle.Package", "Plant3D", 2026, "x64", "pilot", version, "lifecycle-test", true);
|
private static PackageRequirement Req(string version) => new("Lifecycle.Package", "Plant3D", 2026, "x64", "pilot", version, "lifecycle-test", true);
|
||||||
private static PackageManifest Manifest(string version, byte[] a, byte[] b) => new(1, "Lifecycle.Package", version, "Plant3D", 2026, "x64", "entry.dll", [
|
private static PackageManifest Manifest(string version, byte[] a, byte[] b) => new(1, "Lifecycle.Package", version, "Plant3D", 2026, "x64", "entry.dll", [
|
||||||
new("entry.dll", a.Length, Convert.ToHexString(SHA256.HashData(a))),
|
new("entry.dll", a.Length, Convert.ToHexString(SHA256.HashData(a))),
|
||||||
@@ -73,6 +84,21 @@ public sealed class PackageLifecycleTests
|
|||||||
finally { if (Directory.Exists(root)) Directory.Delete(root, true); }
|
finally { if (Directory.Exists(root)) Directory.Delete(root, true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StrictPointerBindsIndividualPayloadsAndExactLkgRejectsTampering()
|
||||||
|
{
|
||||||
|
var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N"));var source=Path.Combine(root,"catalog");Directory.CreateDirectory(source);var bytes=new byte[]{4,5};await File.WriteAllBytesAsync(Path.Combine(source,"entry.dll"),bytes);await File.WriteAllBytesAsync(Path.Combine(source,"payload.dat"),bytes);var store=new LocalPackageStore(root,new Sha256FileHasher());
|
||||||
|
try { var p=await store.MaterializeAsync(Manifest("1.0.0",bytes,bytes),source,CancellationToken.None);await store.BindAsync(Req("1.0.0"),"1.0.0",CancellationToken.None);await store.ActivateImmutableAsync(p,Req("1.0.0"),CancellationToken.None);var pointerPath=Path.Combine(root,"Applications","Lifecycle.Package","current.json");using(var doc=JsonDocument.Parse(await File.ReadAllTextAsync(pointerPath))){Assert.Equal(2,doc.RootElement.GetProperty("SchemaVersion").GetInt32());Assert.Equal(2,doc.RootElement.GetProperty("PayloadFiles").GetArrayLength());}Assert.NotNull(await store.ResolveAsync(Req("1.0.0"),CancellationToken.None));var json=await File.ReadAllTextAsync(pointerPath);json=json.Replace("PayloadSetSha256\":\"","PayloadSetSha256\":\"00",StringComparison.Ordinal);await File.WriteAllTextAsync(pointerPath,json);Assert.Null(await store.ResolveAsync(Req("1.0.0"),CancellationToken.None)); }
|
||||||
|
finally{if(Directory.Exists(root))Directory.Delete(root,true);}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(PackageLifecycleFaultPoint.BeforePointerTempWrite)]
|
||||||
|
[InlineData(PackageLifecycleFaultPoint.BeforePointerReplace)]
|
||||||
|
[InlineData(PackageLifecycleFaultPoint.BeforePointerReadback)]
|
||||||
|
public async Task PointerFaultsDoNotReturnFalseActivationAndReleaseLock(PackageLifecycleFaultPoint point)
|
||||||
|
{ var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N"));var source=Path.Combine(root,"catalog");Directory.CreateDirectory(source);await File.WriteAllBytesAsync(Path.Combine(source,"entry.dll"),[1]);await File.WriteAllBytesAsync(Path.Combine(source,"payload.dat"),[2]);try{var package=await new LocalPackageStore(root,new Sha256FileHasher()).MaterializeAsync(Manifest("1.0.0",[1],[2]),source,CancellationToken.None);var failing=new LocalPackageStore(root,new Sha256FileHasher(),new Fault(point));await Assert.ThrowsAnyAsync<Exception>(()=>failing.ActivateImmutableAsync(package,Req("1.0.0"),CancellationToken.None));var healthy=new LocalPackageStore(root,new Sha256FileHasher());var result=await healthy.ActivateImmutableAsync(package,Req("1.0.0"),CancellationToken.None);Assert.Contains(result.Outcome,new[]{"Activated","AlreadyActiveExact"});}finally{if(Directory.Exists(root))Directory.Delete(root,true);} }
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OldReceiptCannotRollbackNewerActivation()
|
public async Task OldReceiptCannotRollbackNewerActivation()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user