From 32607e2f3463abcb3d237eff1469279e6636fe0e Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 3 Aug 2026 17:19:01 +0300 Subject: [PATCH] fix: defer Plant activation until document is quiescent --- .../FirstLimitedApplyAutomation.cs | 38 +++++++++++++------ .../PRecAnnot.Core.Tests/FoundationTests.cs | 16 ++++++++ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/FirstLimitedApplyAutomation.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/FirstLimitedApplyAutomation.cs index 6164b18..08b3758 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/FirstLimitedApplyAutomation.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/FirstLimitedApplyAutomation.cs @@ -139,6 +139,8 @@ internal sealed class FirstLimitedApplyAutomationCoordinator private CoordinatorState previousState = CoordinatorState.Created; private bool documentHandlersSubscribed; private bool lifecycleHandlersSubscribed; + private bool fixtureDocumentActivated; + private bool projectActivationStarted; private bool exitQueued; private bool exitHandlerSubscribed; private string? exceptionText; @@ -155,24 +157,24 @@ internal sealed class FirstLimitedApplyAutomationCoordinator return; } if (string.IsNullOrWhiteSpace(config.OrthoDrawingPath)) throw new InvalidOperationException("AUTOMATION_ORTHO_PATH_REQUIRED"); - transitions.Add("ProjectActivationStarted"); - var projectXml = Path.Combine(Path.GetDirectoryName(config.FixtureMarkerPath)!, "Project.xml"); - var project = PlantProject.LoadProject(projectXml, false, string.Empty, string.Empty); - PlantApplication.SetCurrentProject(project, true); - transitions.Add("ProjectActivated"); + // A first startup Idle is sufficient to arm automation, but not to + // call Plant APIs. Establish an AutoCAD document first and wait for + // its activation plus a quiescent callback before project activation. + AcadApplication.DocumentManager.DocumentActivated += OnDocumentActivated; + AcadApplication.Idle += OnIdle; + lifecycleHandlersSubscribed = true; if (!string.IsNullOrWhiteSpace(config.ModelDrawingPath)) automationOpenedDocuments.Add(AcadApplication.DocumentManager.Open(config.ModelDrawingPath, false)); transitions.Add("DocumentOpenStarted"); + RuntimeDiagnostics.Write("AutomationBeforeDocumentOpen", resultStatus: "PostStartupReady"); document = AcadApplication.DocumentManager.Open(config.OrthoDrawingPath, false); automationOpenedDocuments.Add(document); AcadApplication.DocumentManager.MdiActiveDocument = document; transitions.Add("DocumentOpened"); + RuntimeDiagnostics.Write("AutomationAfterDocumentOpen", resultStatus: "WaitingForDocumentActivation", document: document); document.CommandEnded += OnCommandEnded; document.CommandFailed += OnCommandFailed; document.CommandCancelled += OnCommandCancelled; - AcadApplication.DocumentManager.DocumentActivated += OnDocumentActivated; - AcadApplication.Idle += OnIdle; documentHandlersSubscribed = true; - lifecycleHandlersSubscribed = true; Transition(CoordinatorState.Running, "AutomationRunning"); } catch (System.Exception ex) { FailFromCallback("Start", ex, "AUTOMATION_START_FAILED"); } @@ -188,23 +190,35 @@ internal sealed class FirstLimitedApplyAutomationCoordinator RequestControlledExit(idleSafe: true); } - private void OnIdle(object? sender, EventArgs e) => RunCallback("Idle", BeginContextVerification); + private void OnIdle(object? sender, EventArgs e) => RunCallback("Idle", TryActivateProjectAndVerifyContext); private void OnDocumentActivated(object sender, DocumentCollectionEventArgs e) { RunCallback("DocumentActivated", () => { if (document is null || e.Document is null) return; - if (string.Equals(e.Document.Name, document.Name, StringComparison.OrdinalIgnoreCase)) BeginContextVerification(); + if (!string.Equals(e.Document.Name, document.Name, StringComparison.OrdinalIgnoreCase)) return; + fixtureDocumentActivated = true; + transitions.Add("FixtureDocumentActivated"); + TryActivateProjectAndVerifyContext(); }); } - private void BeginContextVerification() + private void TryActivateProjectAndVerifyContext() { - if (IsTerminal || document is null) return; + if (IsTerminal || document is null || !fixtureDocumentActivated || projectActivationStarted) return; + if (!AcadApplication.IsQuiescent) return; + projectActivationStarted = true; AcadApplication.Idle -= OnIdle; AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated; lifecycleHandlersSubscribed = false; try { + transitions.Add("ProjectActivationStarted"); + RuntimeDiagnostics.Write("AutomationBeforeProjectActivation", resultStatus: "DocumentActivatedAndQuiescent", document: document); + var projectXml = Path.Combine(Path.GetDirectoryName(config!.FixtureMarkerPath)!, "Project.xml"); + var project = PlantProject.LoadProject(projectXml, false, string.Empty, string.Empty); + PlantApplication.SetCurrentProject(project, true); + transitions.Add("ProjectActivated"); + RuntimeDiagnostics.Write("AutomationAfterProjectActivation", resultStatus: "ProjectAvailable", document: document); transitions.Add("ContextVerificationStarted"); var context = new PlantRuntimeContextReader().ReadCurrentContext(document); if (!context.HasPlantProject || !context.IsOrthoDrawing || context.Viewports.Count != 1) { Complete(false, $"AUTOMATION_ORTHO_CONTEXT_INVALID:P={context.HasPlantProject};O={context.IsOrthoDrawing};V={context.Viewports.Count}"); return; } diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs index 5c4d0e7..e3d669a 100644 --- a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs @@ -82,6 +82,22 @@ public sealed class FoundationTests Assert.Contains("PostStartupReady", source); } + [Fact] public void AutomationProjectActivation_WaitsForFixtureDocumentAndQuiescence() + { + var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs")); + var activation = source[source.IndexOf("private void TryActivateProjectAndVerifyContext", StringComparison.Ordinal)..]; + Assert.Contains("!fixtureDocumentActivated", activation); + Assert.Contains("!AcadApplication.IsQuiescent", activation); + Assert.True(activation.IndexOf("PlantProject.LoadProject", StringComparison.Ordinal) > activation.IndexOf("AutomationBeforeProjectActivation", StringComparison.Ordinal)); + } + + [Fact] public void AutomationDocumentActivation_IsSubscribedBeforeFixtureOpen() + { + var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs")); + Assert.True(source.IndexOf("DocumentActivated += OnDocumentActivated", StringComparison.Ordinal) < source.IndexOf("DocumentManager.Open(config.OrthoDrawingPath", StringComparison.Ordinal)); + Assert.Contains("FixtureDocumentActivated", source); + } + [Fact] public void AutomationControlledExit_QueuesNativeQuitAfterPublicCommand() { var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));