diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/FirstLimitedApplyAutomation.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/FirstLimitedApplyAutomation.cs index 4f25871..156d1a6 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/FirstLimitedApplyAutomation.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/FirstLimitedApplyAutomation.cs @@ -128,6 +128,7 @@ internal sealed class FirstLimitedApplyAutomationCoordinator private int? before, after1, after2, afterUndo; private AutomationEntityCounts? baseline, counts1, counts2, countsUndo; private readonly List transitions = ["Created"]; + private bool exitQueued; public void Start() { @@ -164,8 +165,7 @@ internal sealed class FirstLimitedApplyAutomationCoordinator { transitions.Add("LifecycleProbeCompleted"); WriteEvidence(true, null); - transitions.Add("ExitRequested"); - AcadApplication.Quit(); + RequestControlledExit(); } private void OnIdle(object? sender, EventArgs e) @@ -256,12 +256,40 @@ internal sealed class FirstLimitedApplyAutomationCoordinator AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated; WriteEvidence(success, error); if (document is not null) document.CloseAndDiscard(); - AcadApplication.Quit(); + RequestControlledExit(); } catch { } finally { selectedTargetSnapshotId = null; pxpResults.Clear(); } } + // AutoCAD's direct Application.Quit can leave a startup-script host alive. + // Dispatching the native QUIT command from the next quiescent Idle cycle + // preserves normal command shutdown semantics. + private void RequestControlledExit() + { + if (exitQueued) return; + exitQueued = true; + transitions.Add("ExitQueued"); + AcadApplication.Idle += OnExitIdle; + } + + private void OnExitIdle(object? sender, EventArgs args) + { + AcadApplication.Idle -= OnExitIdle; + try + { + var active = AcadApplication.DocumentManager.MdiActiveDocument; + if (active is null) { AcadApplication.Quit(); return; } + transitions.Add("NativeQuitDispatched"); + active.SendStringToExecute("_.QUIT _N ", true, false, false); + } + catch (System.Exception exception) + { + RuntimeDiagnostics.Write("AutomationControlledExitFailed", "ERROR", resultStatus: "AUTOMATION_CONTROLLED_EXIT_FAILED", exception: exception); + AcadApplication.Quit(); + } + } + private void WriteEvidence(bool success, string? error) { if (config is null) 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 333c565..de3a17e 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,14 @@ public sealed class FoundationTests Assert.Contains("PostStartupReady", source); } + [Fact] public void AutomationControlledExit_UsesNativeQuitFromIdle() + { + var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs")); + Assert.Contains("RequestControlledExit", source); + Assert.Contains("AcadApplication.Idle += OnExitIdle", source); + Assert.Contains("active.SendStringToExecute(\"_.QUIT _N \", true, false, false)", source); + } + [Fact] public void PrimaryCommand_IsRegistered() => Assert.Contains("[CommandMethod(\"PHP_REC_ANNOT\")]", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs"))); [Fact] public void LegacyAliases_AreRegistered() => Assert.True(File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-AAV") && File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-OA")); [Fact] public void RibbonCommand_ReferencesRegisteredCommand() => Assert.Contains("PHP_REC_ANNOT ", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));