fix: allocate pilot session before fixture hashing
This commit is contained in:
@@ -86,13 +86,18 @@ internal static class PilotCli
|
||||
private static async Task<int> AutomateFirstApplyAsync(string[] args)
|
||||
{
|
||||
if (!TryParseAutomation(args, out var options, out var error)) return Fail(Blocked, error!);
|
||||
var journal = await AutomationJournal.AllocateAsync(options.PilotRoot, "NextPilotPreview", CancellationToken.None);
|
||||
await journal.RecordAsync("ArgumentsParsed", "Completed", null, CancellationToken.None);
|
||||
if (!TryReadConfiguration(options.PilotRoot, out var baseConfig, out error)) return Fail(Blocked, error!);
|
||||
await journal.RecordAsync("ConfigurationLoaded", "Completed", null, CancellationToken.None);
|
||||
if (!TryResolvePaths(baseConfig, out var basePaths, out error)) return Fail(Blocked, error!);
|
||||
if (baseConfig.LauncherMode != LauncherMode.NextPilot || !baseConfig.PilotEnabled || !baseConfig.AllowProcessStart || !baseConfig.RequireExplicitExecute || !baseConfig.ForceAfterEvidence) return Fail(Blocked, "AUTOMATION_SAFETY_GATE_REJECTED");
|
||||
if (!TryBuildManifest(options.PackageSource, options.EntryAssembly, out var manifest, out var manifestHash, out error)) return Fail(Blocked, error!);
|
||||
if (Process.GetProcessesByName("acad").Any()) return Fail(AcadRunning, "ACAD_ALREADY_RUNNING");
|
||||
Directory.CreateDirectory(options.EvidenceRoot);
|
||||
var before = ContentHashes(options.FixtureMarker);
|
||||
await journal.RecordAsync("HashPreparationStarted", "Started", null, CancellationToken.None);
|
||||
var before = await ContentHashesAsync(options.FixtureMarker, journal, CancellationToken.None);
|
||||
await journal.RecordAsync("HashPreparationCompleted", "Completed", null, CancellationToken.None);
|
||||
var previewConfigPath = Path.Combine(options.EvidenceRoot, "automation-preview.config.json");
|
||||
var previewEvidence = Path.Combine(options.EvidenceRoot, "preview-result.json");
|
||||
var entryHash = manifest!.Files.Single(x => string.Equals(x.Path, options.EntryAssembly, StringComparison.OrdinalIgnoreCase)).Sha256;
|
||||
@@ -103,7 +108,7 @@ internal static class PilotCli
|
||||
if (!previewSession.Started || !previewSession.RollbackVerified || !previewSession.EvidenceCompleted) return Fail(ProcessFailed, "AUTOMATION_PREVIEW_SESSION_FAILED");
|
||||
var preview = JsonSerializer.Deserialize<PreviewEvidence>(await File.ReadAllTextAsync(previewEvidence), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
if (preview is null || !preview.Completed || preview.CommandsCompleted != true || preview.InspectorsCompleted != true || preview.EvidenceFlushed != true || preview.PlanIdRun1 is null || preview.PlanIdRun1 != preview.PlanIdRun2 || preview.PlanIdsEqual != true || preview.EligibleCandidateCount is not > 0 || string.IsNullOrWhiteSpace(preview.SelectedCandidateHash)) return Fail(Blocked, "AUTOMATION_PREVIEW_EVIDENCE_INVALID");
|
||||
if (!HashesEqual(before, ContentHashes(options.FixtureMarker))) return Fail(Blocked, "AUTOMATION_PREVIEW_CONTENT_CHANGED");
|
||||
if (!HashesEqual(before, await ContentHashesAsync(options.FixtureMarker, journal, CancellationToken.None))) return Fail(Blocked, "AUTOMATION_PREVIEW_CONTENT_CHANGED");
|
||||
// A diagnostic preview establishes the script/registration boundary only.
|
||||
// It must never mint an Apply permit or start an Apply process.
|
||||
if (options.Diagnostic) return Success;
|
||||
@@ -115,7 +120,7 @@ internal static class PilotCli
|
||||
await File.WriteAllTextAsync(applyEvidence, "{\"completed\":false}");
|
||||
var applyConfig = WithAutomationPaths(baseConfig, options, applyConfigPath, applyEvidence, permitPath, "apply") with { PluginExecutionMode = PluginExecutionMode.Apply, AllowApply = true, ApplyPermitPath = permitPath, AutomationMode = "FirstLimitedApplyRuntimeTest", AutomationConfigurationPath = applyConfigPath, FixtureMarkerPath = options.FixtureMarker, LaunchMode = AcadLaunchMode.NextPilotApply, AutomationDiagnostic = false };
|
||||
var applySession = await RunAutomationSession(applyConfig, manifest!, manifestHash!, options, "apply", applyEvidence);
|
||||
var hashesEqual = HashesEqual(before, ContentHashes(options.FixtureMarker));
|
||||
var hashesEqual = HashesEqual(before, await ContentHashesAsync(options.FixtureMarker, journal, CancellationToken.None));
|
||||
var apply = File.Exists(applyEvidence) ? JsonSerializer.Deserialize<ApplyEvidence>(await File.ReadAllTextAsync(applyEvidence), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) : null;
|
||||
if (File.Exists(permitPath)) File.Delete(permitPath);
|
||||
var applyVerified = apply is { Completed: true, CommandsCompleted: true, InspectorsCompleted: true, EvidenceFlushed: true, PermitAccepted: true, ApplyOutcomeRun1: "Created", ApplyOutcomeRun2: "AlreadyExists" } && apply.Baseline is not null && apply.AfterCommand1 is not null && apply.AfterCommand2 is not null && apply.AfterUndo is not null;
|
||||
@@ -183,7 +188,24 @@ internal static class PilotCli
|
||||
if (!File.Exists(V("--annotation-template-dwg"))) { error = "ANNOTATION_TEMPLATE_ASSET_MISSING"; return false; }
|
||||
options = new(V("--package-source")!, V("--entry-assembly")!, V("--pilot-root")!, V("--fixture-marker")!, V("--ortho-dwg")!, V("--model-dwg")!, V("--annotation-template-dwg")!, V("--evidence-root")!, diagnostic); return true;
|
||||
}
|
||||
private static Dictionary<string,string> ContentHashes(string marker) { var root = Path.GetDirectoryName(marker)!; return Directory.GetFiles(root, "*", SearchOption.AllDirectories).Where(x => x.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase) || x.EndsWith(".dcf", StringComparison.OrdinalIgnoreCase) || Path.GetFileName(x).Equals("Project.xml", StringComparison.OrdinalIgnoreCase)).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToDictionary(x => Path.GetRelativePath(root,x), x => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(x))), StringComparer.OrdinalIgnoreCase); }
|
||||
private static async Task<Dictionary<string,string>> ContentHashesAsync(string marker, AutomationJournal journal, CancellationToken cancellationToken)
|
||||
{
|
||||
var root = Path.GetDirectoryName(marker)!;
|
||||
var files = new[] { Path.Combine(root, "Piping.dcf"), Path.Combine(root, "Project.xml") }
|
||||
.Concat(Directory.EnumerateFiles(Path.Combine(root, "Orthos", "DWGs"), "*.dwg", SearchOption.TopDirectoryOnly))
|
||||
.Concat(Directory.EnumerateFiles(Path.Combine(root, "Plant 3D Models"), "*.dwg", SearchOption.TopDirectoryOnly))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var result = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var path in files) { if (!File.Exists(path)) throw new InvalidOperationException("HASH_FILE_NOT_FOUND"); await journal.RecordAsync("HashFileStarted", "Started", path, cancellationToken); await using var stream = File.OpenRead(path); result[Path.GetRelativePath(root,path)] = Convert.ToHexString(await SHA256.HashDataAsync(stream,cancellationToken)); await journal.RecordAsync("HashFileCompleted", "Completed", path, cancellationToken); }
|
||||
return result;
|
||||
}
|
||||
private sealed class AutomationJournal
|
||||
{
|
||||
private readonly string path; private readonly DateTimeOffset started=DateTimeOffset.UtcNow; private int sequence;
|
||||
private AutomationJournal(string path) { this.path=path; }
|
||||
public static async Task<AutomationJournal> AllocateAsync(string root,string mode,CancellationToken ct) { var id=Guid.NewGuid().ToString("N"); var dir=Path.Combine(root,"state","automation-sessions",id); Directory.CreateDirectory(dir); var j=new AutomationJournal(Path.Combine(dir,"session-events.jsonl")); await j.RecordAsync("SessionAllocated","Completed",null,ct); var pointer=Path.Combine(root,"state","automation-current.json"); var temp=pointer+".tmp"; await File.WriteAllTextAsync(temp,JsonSerializer.Serialize(new { sessionId=id,launchMode=mode,state="SessionAllocated",terminal=false,currentStage="InitialEvidenceFlushed",createdUtc=DateTimeOffset.UtcNow }),ct); File.Move(temp,pointer,true); await j.RecordAsync("InitialEvidenceFlushed","Completed",null,ct); return j; }
|
||||
public Task RecordAsync(string stage,string status,string? currentPath,CancellationToken ct) => File.AppendAllTextAsync(path,JsonSerializer.Serialize(new { sequence=Interlocked.Increment(ref sequence),timestampUtc=DateTimeOffset.UtcNow,elapsedMs=(long)(DateTimeOffset.UtcNow-started).TotalMilliseconds,stage,status,canonicalPath=currentPath,managedThreadId=Environment.CurrentManagedThreadId })+Environment.NewLine,ct);
|
||||
}
|
||||
private static bool HashesEqual(IReadOnlyDictionary<string,string> x, IReadOnlyDictionary<string,string> y) => x.Count == y.Count && x.All(p => y.TryGetValue(p.Key, out var value) && value == p.Value);
|
||||
private static PilotExecutionService CreateService(ResolvedLauncherPaths paths) => new(new PackageManifestValidator(), new PilotRootPolicy(paths.LocalLauncherRoot), new NextPilotSafetyGate(), new Sha256FileHasher(), new StartupScriptBuilder(), PermitService, new Probe(), new OperatingSystemProcessInvoker(), new PathResolver());
|
||||
private static async Task WaitForExitAsync(int processId) { using var process = Process.GetProcessById(processId); await process.WaitForExitAsync(); }
|
||||
|
||||
Reference in New Issue
Block a user