fix: preserve completed automation evidence when quit is unavailable
This commit is contained in:
@@ -102,7 +102,16 @@ internal sealed record FirstApplyAutomationEvidence
|
||||
public int? CreateRun1 { get; init; }
|
||||
public int? CreateRun2 { get; init; }
|
||||
public int? SkipRun2 { get; init; }
|
||||
public string? ApplyOutcomeRun1 { get; init; }
|
||||
public string? ApplyOutcomeRun2 { get; init; }
|
||||
public bool? PermitAccepted { get; init; }
|
||||
public bool? CommandsCompleted { get; init; }
|
||||
public bool? InspectorsCompleted { get; init; }
|
||||
public bool? EvidenceFlushed { get; init; }
|
||||
public bool? ExitAttemptQueued { get; init; }
|
||||
public bool? QuitCommandWillStart { get; init; }
|
||||
public bool? QuitCommandEnded { get; init; }
|
||||
public string? ExitFailureCode { get; init; }
|
||||
public int? IdleCallbackCount { get; init; }
|
||||
public int? CoordinatorStartCount { get; init; }
|
||||
// Local test evidence only. This is intentionally never copied into Git
|
||||
@@ -116,7 +125,7 @@ internal sealed record FirstApplyAutomationEvidence
|
||||
internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||
{
|
||||
private enum CoordinatorState { Created, Starting, Running, Completed, Failed, ExitQueued }
|
||||
private sealed record PxpResult(string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, bool? PermitAccepted);
|
||||
private sealed record PxpResult(string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, bool? PermitAccepted, string? ApplyOutcome);
|
||||
private static string? selectedTargetSnapshotId;
|
||||
private static readonly List<PxpResult> pxpResults = [];
|
||||
internal static string? SelectedTargetSnapshotId => selectedTargetSnapshotId;
|
||||
@@ -125,7 +134,13 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||
internal static void RecordPxpOa(string? planId, IReadOnlyList<string> candidates, string? view, int create, int skip, bool? permitAccepted)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal))
|
||||
pxpResults.Add(new(planId, candidates, view, create, skip, permitAccepted));
|
||||
pxpResults.Add(new(planId, candidates, view, create, skip, permitAccepted, null));
|
||||
}
|
||||
internal static void RecordApplyOutcome(PipeAnnotationWriteOutcome outcome)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal) || pxpResults.Count == 0) return;
|
||||
var last = pxpResults[^1];
|
||||
pxpResults[^1] = last with { ApplyOutcome = outcome.ToString() };
|
||||
}
|
||||
private FirstApplyAutomationConfiguration? config;
|
||||
private Document? document;
|
||||
@@ -144,6 +159,10 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||
private bool exitQueued;
|
||||
private bool exitHandlerSubscribed;
|
||||
private string? exceptionText;
|
||||
private bool functionalCompleted;
|
||||
private bool evidenceFlushed;
|
||||
private bool exitAttemptQueued;
|
||||
private string? exitFailureCode;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
@@ -293,6 +312,7 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||
private void Complete(bool success, string? error, bool safeToExitNow = false)
|
||||
{
|
||||
if (IsTerminal) return;
|
||||
functionalCompleted = success;
|
||||
Transition(success ? CoordinatorState.Completed : CoordinatorState.Failed, success ? "Completed" : "Failed");
|
||||
UnsubscribeHandlers();
|
||||
TryWriteEvidence(success, error);
|
||||
@@ -330,16 +350,29 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||
{
|
||||
if (exitHandlerSubscribed) AcadApplication.Idle -= OnExitIdle;
|
||||
exitHandlerSubscribed = false;
|
||||
exitAttemptQueued = true;
|
||||
if (!AcadApplication.IsQuiescent)
|
||||
{
|
||||
transitions.Add("QuitDispatchDeferredNotQuiescent");
|
||||
TryWriteEvidence(functionalCompleted, null);
|
||||
return;
|
||||
}
|
||||
// LifecycleProbe opens no fixture document, but the real startup
|
||||
// document is available at this point (observed as Drawing1.dwg).
|
||||
// Use that live document only to queue native QUIT; never retain it
|
||||
// past this callback or use it for fixture/Plant work.
|
||||
var exitDocument = document ?? AcadApplication.DocumentManager.MdiActiveDocument;
|
||||
if (exitDocument is null) throw new InvalidOperationException("AUTOMATION_EXIT_DOCUMENT_UNAVAILABLE");
|
||||
transitions.Add("NativeQuitCommandQueued");
|
||||
var exitDocument = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||
if (exitDocument is null)
|
||||
{
|
||||
exitFailureCode = "AUTOMATION_EXIT_DOCUMENT_UNAVAILABLE";
|
||||
transitions.Add("QuitDispatchSkippedNoLiveDocument");
|
||||
TryWriteEvidence(functionalCompleted, null);
|
||||
return;
|
||||
}
|
||||
transitions.Add("QuitDispatchAttempted");
|
||||
// Persist the exit boundary before handing control back to AutoCAD;
|
||||
// process termination can otherwise prevent the final evidence flush.
|
||||
TryWriteEvidence(state == CoordinatorState.Completed, null);
|
||||
TryWriteEvidence(functionalCompleted, null);
|
||||
exitDocument.SendStringToExecute("_.QUIT _N ", true, false, false);
|
||||
document = null;
|
||||
automationOpenedDocuments.Clear();
|
||||
@@ -348,7 +381,11 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||
{
|
||||
// This is terminal cleanup: contain the exception and leave process
|
||||
// cleanup to Launcher rather than re-entering document APIs.
|
||||
FailFromCallback("ControlledExitIdle", exception, "AUTOMATION_CONTROLLED_EXIT_FAILED");
|
||||
// A failed graceful-exit dispatch is not a failure of already
|
||||
// completed PXP-OA/UNDO work. Launcher owns bounded forced cleanup.
|
||||
exitFailureCode = "AUTOMATION_CONTROLLED_EXIT_FAILED:" + exception.GetType().Name;
|
||||
transitions.Add("QuitDispatchFailed");
|
||||
TryWriteEvidence(functionalCompleted, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,7 +413,13 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||
else
|
||||
{
|
||||
UnsubscribeHandlers();
|
||||
TryWriteEvidence(false, code + ":" + callback + ":" + exception.GetType().Name);
|
||||
if (functionalCompleted)
|
||||
{
|
||||
exitFailureCode = code + ":" + callback + ":" + exception.GetType().Name;
|
||||
transitions.Add("ExitCallbackFailed");
|
||||
TryWriteEvidence(true, null);
|
||||
}
|
||||
else TryWriteEvidence(false, code + ":" + callback + ":" + exception.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,17 +439,18 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||
lifecycleHandlersSubscribed = false;
|
||||
}
|
||||
|
||||
private void TryWriteEvidence(bool success, string? error)
|
||||
private bool TryWriteEvidence(bool success, string? error)
|
||||
{
|
||||
try { WriteEvidence(success, error); }
|
||||
catch (System.Exception exception) { RuntimeDiagnostics.Write("AutomationEvidenceWriteFailed", "ERROR", resultStatus: "AUTOMATION_EVIDENCE_WRITE_FAILED", exception: exception); }
|
||||
try { evidenceFlushed = true; WriteEvidence(success, error); return true; }
|
||||
catch (System.Exception exception) { evidenceFlushed = false; RuntimeDiagnostics.Write("AutomationEvidenceWriteFailed", "ERROR", resultStatus: "AUTOMATION_EVIDENCE_WRITE_FAILED", exception: exception); return false; }
|
||||
}
|
||||
|
||||
private void WriteEvidence(bool success, string? error)
|
||||
{
|
||||
if (config is null) return;
|
||||
var first = pxpResults.ElementAtOrDefault(0); var second = pxpResults.ElementAtOrDefault(1);
|
||||
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = success, ErrorCode = error, DbmodBefore = before, DbmodAfterCommand1 = after1, DbmodAfterCommand2 = after2, DbmodAfterUndo = afterUndo, Baseline = baseline, AfterCommand1 = counts1, AfterCommand2 = counts2, AfterUndo = countsUndo, CompletedCommands = completed, PlanIdRun1 = first?.PlanId, PlanIdRun2 = second?.PlanId, PlanIdsEqual = first is not null && second is not null ? string.Equals(first.PlanId, second.PlanId, StringComparison.Ordinal) : null, EligibleCandidateCount = first?.Candidates.Count, SelectedCandidateHash = selectedTargetSnapshotId, ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, PermitAccepted = first?.PermitAccepted, IdleCallbackCount = FirstLimitedApplyAutomationBootstrap.IdleCallbackCount, CoordinatorStartCount = FirstLimitedApplyAutomationBootstrap.CoordinatorStartCount, ExceptionText = exceptionText, PreviousState = previousState.ToString(), CurrentState = state.ToString(), StateTransitions = transitions };
|
||||
var expectedCommands = string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase) ? 3 : 2;
|
||||
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = success, ErrorCode = error, DbmodBefore = before, DbmodAfterCommand1 = after1, DbmodAfterCommand2 = after2, DbmodAfterUndo = afterUndo, Baseline = baseline, AfterCommand1 = counts1, AfterCommand2 = counts2, AfterUndo = countsUndo, CompletedCommands = completed, PlanIdRun1 = first?.PlanId, PlanIdRun2 = second?.PlanId, PlanIdsEqual = first is not null && second is not null ? string.Equals(first.PlanId, second.PlanId, StringComparison.Ordinal) : null, EligibleCandidateCount = first?.Candidates.Count, SelectedCandidateHash = selectedTargetSnapshotId, ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, ApplyOutcomeRun1 = first?.ApplyOutcome, ApplyOutcomeRun2 = second?.ApplyOutcome, PermitAccepted = first?.PermitAccepted, CommandsCompleted = completed.Count >= expectedCommands, InspectorsCompleted = baseline is not null && counts1 is not null && counts2 is not null && (!string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase) || countsUndo is not null), EvidenceFlushed = evidenceFlushed, ExitAttemptQueued = exitAttemptQueued, ExitFailureCode = exitFailureCode, IdleCallbackCount = FirstLimitedApplyAutomationBootstrap.IdleCallbackCount, CoordinatorStartCount = FirstLimitedApplyAutomationBootstrap.CoordinatorStartCount, ExceptionText = exceptionText, PreviousState = previousState.ToString(), CurrentState = state.ToString(), StateTransitions = transitions };
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!);
|
||||
File.WriteAllText(config.EvidencePath, JsonSerializer.Serialize(evidence));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user