fix: queue native quit after automation commands

This commit is contained in:
codex
2026-08-03 17:14:14 +03:00
parent 33f91c26aa
commit c9c4113e86
2 changed files with 17 additions and 23 deletions
@@ -287,9 +287,10 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
RequestControlledExit(safeToExitNow); RequestControlledExit(safeToExitNow);
} }
// AutoCAD's direct Application.Quit can leave a startup-script host alive. // The managed Application.Quit call can leave a startup-script host alive
// Dispatching the native QUIT command from the next quiescent Idle cycle // when it is invoked from a CommandEnded callback. Queue AutoCAD's native
// preserves normal command shutdown semantics. // QUIT command instead; the command processor runs it after the current
// public PXP-OA command has fully returned and discards every open drawing.
private void RequestControlledExit(bool idleSafe = false) private void RequestControlledExit(bool idleSafe = false)
{ {
if (exitQueued) return; if (exitQueued) return;
@@ -315,21 +316,14 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
{ {
if (exitHandlerSubscribed) AcadApplication.Idle -= OnExitIdle; if (exitHandlerSubscribed) AcadApplication.Idle -= OnExitIdle;
exitHandlerSubscribed = false; exitHandlerSubscribed = false;
// Do not close a cached automation document and then reuse it. Resolve if (document is null) throw new InvalidOperationException("AUTOMATION_EXIT_DOCUMENT_UNAVAILABLE");
// the currently-live document only at the final quiescent callback. transitions.Add("NativeQuitCommandQueued");
// Only documents opened by this coordinator may be closed. Keep no // Persist the exit boundary before handing control back to AutoCAD;
// document reference after closing it; DocumentDestroyed can run // process termination can otherwise prevent the final evidence flush.
// immediately as part of CloseAndDiscard. TryWriteEvidence(state == CoordinatorState.Completed, null);
foreach (var opened in automationOpenedDocuments.ToArray().Reverse()) document.SendStringToExecute("_.QUIT _N ", true, false, false);
{ document = null;
transitions.Add("AutomationDocumentCloseRequested");
opened.CloseAndDiscard();
}
automationOpenedDocuments.Clear(); automationOpenedDocuments.Clear();
// Application.Quit alone leaves the script host alive. Close the
// live document first, then request normal application shutdown.
transitions.Add("ApplicationQuitRequested");
AcadApplication.Quit();
} }
catch (System.Exception exception) catch (System.Exception exception)
{ {
@@ -82,14 +82,14 @@ public sealed class FoundationTests
Assert.Contains("PostStartupReady", source); Assert.Contains("PostStartupReady", source);
} }
[Fact] public void AutomationControlledExit_ClosesOnlyAutomationDocumentsThenQuits() [Fact] public void AutomationControlledExit_QueuesNativeQuitAfterPublicCommand()
{ {
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs")); var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
Assert.Contains("RequestControlledExit", source); Assert.Contains("RequestControlledExit", source);
Assert.Contains("AcadApplication.Idle += OnExitIdle", source); Assert.Contains("AcadApplication.Idle += OnExitIdle", source);
Assert.Contains("automationOpenedDocuments", source); Assert.Contains("NativeQuitCommandQueued", source);
Assert.Contains("opened.CloseAndDiscard();", source); Assert.Contains("document.SendStringToExecute(\"_.QUIT _N \", true, false, false);", source);
Assert.Contains("AcadApplication.Quit();", source); Assert.DoesNotContain("AcadApplication.Quit();", source);
} }
[Fact] public void Callback_AfterDocumentDestroyed_DoesNotAccessDocument() [Fact] public void Callback_AfterDocumentDestroyed_DoesNotAccessDocument()
@@ -103,7 +103,7 @@ public sealed class FoundationTests
{ {
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs")); var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
Assert.DoesNotContain("MdiActiveDocument", source[source.IndexOf("private void DispatchNativeQuit", StringComparison.Ordinal)..]); Assert.DoesNotContain("MdiActiveDocument", source[source.IndexOf("private void DispatchNativeQuit", StringComparison.Ordinal)..]);
Assert.Contains("AcadApplication.Quit();", source); Assert.Contains("AUTOMATION_EXIT_DOCUMENT_UNAVAILABLE", source);
} }
[Fact] public void TerminalState_IgnoresFurtherCallbacks() [Fact] public void TerminalState_IgnoresFurtherCallbacks()
@@ -143,8 +143,8 @@ public sealed class FoundationTests
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs")); var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
var controlledExit = source[source.IndexOf("private void RequestControlledExit", StringComparison.Ordinal)..]; var controlledExit = source[source.IndexOf("private void RequestControlledExit", StringComparison.Ordinal)..];
Assert.DoesNotContain("document.CloseAndDiscard", controlledExit); Assert.DoesNotContain("document.CloseAndDiscard", controlledExit);
Assert.Contains("Resolve", controlledExit, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("MdiActiveDocument", controlledExit); Assert.DoesNotContain("MdiActiveDocument", controlledExit);
Assert.Contains("document = null;", controlledExit);
} }
[Fact] public void FinalCommandCompletion_UsesItsQuiescentExitBoundary() [Fact] public void FinalCommandCompletion_UsesItsQuiescentExitBoundary()