refactor: observe commands driven by AutoCAD automation script
This commit is contained in:
@@ -10,24 +10,12 @@ namespace PRecAnnot.Plant3D2026;
|
||||
|
||||
public sealed class EntryPoint : IExtensionApplication
|
||||
{
|
||||
private static bool automationQueued;
|
||||
public void Initialize()
|
||||
{
|
||||
RuntimeDiagnostics.Write("AssemblyInitializeStarted");
|
||||
RibbonIntegration.Register();
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal) && !automationQueued)
|
||||
{
|
||||
try
|
||||
{
|
||||
automationQueued = true;
|
||||
FirstLimitedApplyAutomationBootstrap.Prepare();
|
||||
RuntimeDiagnostics.Write("AutomationBootstrapPrepared", resultStatus: "Gated");
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
RuntimeDiagnostics.Write("AutomationBootstrapPreparationFailed", "ERROR", resultStatus: "AUTOMATION_BOOTSTRAP_FAILED", exception: exception);
|
||||
}
|
||||
}
|
||||
// Test automation is driven only by the explicitly generated /b script.
|
||||
// Initialize remains production-equivalent and must not orchestrate commands.
|
||||
RuntimeDiagnostics.Write("AssemblyInitializeCompleted");
|
||||
}
|
||||
|
||||
@@ -56,6 +44,12 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
[CommandMethod("PHP_REC_AUTOMATION_READY")]
|
||||
public void AutomationReadyCommand() { }
|
||||
|
||||
[CommandMethod("PHP_REC_AUTOMATION_PREPARE", CommandFlags.Session)]
|
||||
public void AutomationPrepareCommand() => ScriptDrivenFirstApplyAutomation.Prepare();
|
||||
|
||||
[CommandMethod("PHP_REC_AUTOMATION_FINALIZE", CommandFlags.Session)]
|
||||
public void AutomationFinalizeCommand() => ScriptDrivenFirstApplyAutomation.FinalizePreview();
|
||||
|
||||
private static void RunDiagnostic(string commandName)
|
||||
{
|
||||
var operationId = Guid.NewGuid().ToString("N");
|
||||
@@ -171,6 +165,7 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
var candidates = objectsByViewport.SelectMany(x => x)
|
||||
.Where(x => string.Equals(x.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase) && x.MembershipStatus == ViewMembershipStatus.Resolved && x.Geometry.PaperPosition is not null)
|
||||
.Select(x => x.SnapshotId).OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||
ScriptDrivenFirstApplyAutomation.RecordPxpOa(planId, candidates, context.Viewports.SingleOrDefault()?.Id, summary.CreateCount, summary.SkipCount, mode == PRecAnnotExecutionMode.Apply ? applyPermit is not null : null, summary.ObjectsExamined, membershipResolved);
|
||||
FirstLimitedApplyAutomationCoordinator.RecordPxpOa(planId, candidates, context.Viewports.SingleOrDefault()?.Id, summary.CreateCount, summary.SkipCount, mode == PRecAnnotExecutionMode.Apply ? applyPermit is not null : null);
|
||||
}
|
||||
if (mode == PRecAnnotExecutionMode.Apply)
|
||||
|
||||
@@ -83,6 +83,8 @@ internal sealed record FirstApplyAutomationEvidence
|
||||
public int SchemaVersion { get; init; } = 1;
|
||||
public required string Phase { get; init; }
|
||||
public bool Completed { get; init; }
|
||||
public bool? Prepared { get; init; }
|
||||
public bool? FunctionalEvidenceCompleted { get; init; }
|
||||
public string? ErrorCode { get; init; }
|
||||
public int? DbmodBefore { get; init; }
|
||||
public int? DbmodAfterCommand1 { get; init; }
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
using System.Text.Json;
|
||||
using Autodesk.AutoCAD.ApplicationServices;
|
||||
using Autodesk.AutoCAD.DatabaseServices;
|
||||
using Autodesk.AutoCAD.Runtime;
|
||||
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||
|
||||
namespace PRecAnnot.Plant3D2026;
|
||||
|
||||
// Test-only observer. The AutoCAD /b processor, never this type, drives PXP-OA.
|
||||
internal static class ScriptDrivenFirstApplyAutomation
|
||||
{
|
||||
private sealed record Config(string Phase, string OrthoDrawingPath, string EvidencePath, string FixtureMarkerPath);
|
||||
private sealed record Run(int Sequence, string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, int? DbmodBefore, int? DbmodAfter, AutomationEntityCounts? Before, AutomationEntityCounts? After, bool WillStart, bool Ended, bool Failed, bool Cancelled, int? SourceRows, int? MembershipResolved);
|
||||
private static Config? config;
|
||||
private static Document? fixture;
|
||||
private static Database? database;
|
||||
private static AutomationEntityCounts? baseline;
|
||||
private static int? dbmodBaseline;
|
||||
private static readonly List<Run> runs = [];
|
||||
private static readonly List<string> transitions = [];
|
||||
private static bool prepared;
|
||||
private static bool terminal;
|
||||
|
||||
internal static void Prepare()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (terminal) return;
|
||||
config = ReadConfig();
|
||||
RequireGate(config);
|
||||
var active = AcadApplication.DocumentManager.MdiActiveDocument ?? throw new InvalidOperationException("AUTOMATION_ACTIVE_DOCUMENT_MISSING");
|
||||
if (!SamePath(active.Name, config.OrthoDrawingPath)) throw new InvalidOperationException("AUTOMATION_UNEXPECTED_ACTIVE_FIXTURE");
|
||||
fixture = active;
|
||||
database = active.Database ?? throw new InvalidOperationException("AUTOMATION_FIXTURE_DATABASE_MISSING");
|
||||
fixture.CommandWillStart += OnWillStart;
|
||||
fixture.CommandEnded += OnEnded;
|
||||
fixture.CommandFailed += OnFailed;
|
||||
fixture.CommandCancelled += OnCancelled;
|
||||
var context = new PlantRuntimeContextReader().ReadCurrentContext(fixture);
|
||||
if (!context.HasPlantProject || !context.IsOrthoDrawing || context.Viewports.Count != 1)
|
||||
throw new InvalidOperationException("AUTOMATION_ORTHO_CONTEXT_INVALID");
|
||||
dbmodBaseline = Dbmod();
|
||||
baseline = Counts(database);
|
||||
prepared = true;
|
||||
transitions.Add("Prepared");
|
||||
Write(false, null);
|
||||
}
|
||||
catch (System.Exception exception) { Fail("AUTOMATION_PREPARE_FAILED", exception); }
|
||||
}
|
||||
|
||||
internal static void RecordPxpOa(string? planId, IReadOnlyList<string> candidates, string? view, int create, int skip, bool? permitAccepted, int sourceRows, int membershipResolved)
|
||||
{
|
||||
if (!prepared || terminal || runs.Count == 0) return;
|
||||
var last = runs[^1];
|
||||
if (!last.WillStart || last.PlanId is not null) return;
|
||||
runs[^1] = last with { PlanId = planId, Candidates = candidates, View = view, Create = create, Skip = skip, SourceRows = sourceRows, MembershipResolved = membershipResolved };
|
||||
}
|
||||
|
||||
internal static void FinalizePreview()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!prepared) throw new InvalidOperationException("AUTOMATION_NOT_PREPARED");
|
||||
if (runs.Count != 2) throw new InvalidOperationException("AUTOMATION_EXPECTS_EXACTLY_TWO_PXP_OA");
|
||||
if (runs.Any(x => !x.WillStart || !x.Ended || x.Failed || x.Cancelled)) throw new InvalidOperationException("AUTOMATION_PXP_OA_NOT_ENDED");
|
||||
if (runs.Any(x => string.IsNullOrWhiteSpace(x.PlanId)) || !string.Equals(runs[0].PlanId, runs[1].PlanId, StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_PLAN_IDS_DIFFER");
|
||||
if (runs.Any(x => x.Create != 1)) throw new InvalidOperationException("AUTOMATION_PREVIEW_CREATE_NOT_ONE");
|
||||
if (runs[0].Candidates.Count == 0) throw new InvalidOperationException("AUTOMATION_NO_ELIGIBLE_CANDIDATE");
|
||||
if (Dbmod() != dbmodBaseline || !Equals(Counts(database!), baseline)) throw new InvalidOperationException("AUTOMATION_INSPECTOR_INVARIANCE_FAILED");
|
||||
terminal = true;
|
||||
transitions.Add("PreviewCompleted");
|
||||
Write(true, null);
|
||||
Unsubscribe();
|
||||
}
|
||||
catch (System.Exception exception) { Fail("PREVIEW_ACCEPTANCE_FAILED", exception); }
|
||||
}
|
||||
|
||||
private static void OnWillStart(object sender, CommandEventArgs args)
|
||||
{
|
||||
Guard(() =>
|
||||
{
|
||||
if (!IsFixture(sender) || !IsPxp(args.GlobalCommandName) || runs.Count >= 2) return;
|
||||
runs.Add(new Run(runs.Count + 1, null, [], null, 0, 0, Dbmod(), null, Counts(database!), null, true, false, false, false, null, null));
|
||||
transitions.Add("PxpOaWillStart:" + runs.Count);
|
||||
});
|
||||
}
|
||||
private static void OnEnded(object sender, CommandEventArgs args) => Finish(sender, args, false, false);
|
||||
private static void OnFailed(object sender, CommandEventArgs args) => Finish(sender, args, true, false);
|
||||
private static void OnCancelled(object sender, CommandEventArgs args) => Finish(sender, args, false, true);
|
||||
private static void Finish(object sender, CommandEventArgs args, bool failed, bool cancelled)
|
||||
{
|
||||
Guard(() =>
|
||||
{
|
||||
if (!IsFixture(sender) || !IsPxp(args.GlobalCommandName) || runs.Count == 0) return;
|
||||
var last = runs[^1];
|
||||
if (last.Ended || last.Failed || last.Cancelled) return;
|
||||
runs[^1] = last with { Ended = !failed && !cancelled, Failed = failed, Cancelled = cancelled, DbmodAfter = Dbmod(), After = Counts(database!) };
|
||||
transitions.Add(failed ? "PxpOaFailed" : cancelled ? "PxpOaCancelled" : "PxpOaEnded:" + last.Sequence);
|
||||
Write(false, null);
|
||||
});
|
||||
}
|
||||
private static bool IsFixture(object sender) => !terminal && fixture is not null && ReferenceEquals(sender, fixture) && database is not null && ReferenceEquals(fixture.Database, database);
|
||||
private static bool IsPxp(string name) => string.Equals(name.TrimStart('.', '_'), "PXP-OA", StringComparison.OrdinalIgnoreCase);
|
||||
private static void Guard(Action action) { try { if (!terminal) action(); } catch (System.Exception ex) { Fail("AUTOMATION_EVENT_HANDLER_FAILED", ex); } }
|
||||
private static void Fail(string code, System.Exception exception) { if (terminal) return; terminal = true; transitions.Add("Failed:" + code); try { Write(false, code + ":" + exception); } finally { Unsubscribe(); } }
|
||||
private static void Unsubscribe() { if (fixture is null) return; try { fixture.CommandWillStart -= OnWillStart; fixture.CommandEnded -= OnEnded; fixture.CommandFailed -= OnFailed; fixture.CommandCancelled -= OnCancelled; } catch { } }
|
||||
private static int? Dbmod() { try { return Convert.ToInt32(AcadApplication.GetSystemVariable("DBMOD")); } catch { return null; } }
|
||||
private static AutomationEntityCounts Counts(Database db)
|
||||
{
|
||||
using var tx = db.TransactionManager.StartTransaction();
|
||||
var space = (BlockTableRecord)tx.GetObject(db.CurrentSpaceId, OpenMode.ForRead);
|
||||
var total = 0; var pipes = 0; var xdata = 0; var owned = 0;
|
||||
foreach (ObjectId id in space) { if (tx.GetObject(id, OpenMode.ForRead) is not Entity entity) continue; total++; if (entity is BlockReference br && string.Equals(br.Name, "PIPE", StringComparison.OrdinalIgnoreCase)) pipes++; if (entity.XData is not null) { foreach (TypedValue value in entity.XData) if (value.Value is string text && string.Equals(text, "PXP-OA", StringComparison.OrdinalIgnoreCase)) { xdata++; owned++; break; } } }
|
||||
return new(total, pipes, xdata, owned);
|
||||
}
|
||||
private static Config ReadConfig()
|
||||
{
|
||||
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION_CONFIG") ?? throw new InvalidOperationException("AUTOMATION_CONFIG_MISSING");
|
||||
using var json = JsonDocument.Parse(File.ReadAllText(path)); var root = json.RootElement;
|
||||
return new(root.GetProperty("phase").GetString() ?? "Preview", root.GetProperty("orthoDrawingPath").GetString() ?? throw new InvalidOperationException("AUTOMATION_ORTHO_PATH_REQUIRED"), root.GetProperty("evidencePath").GetString() ?? throw new InvalidOperationException("AUTOMATION_EVIDENCE_PATH_REQUIRED"), root.GetProperty("fixtureMarkerPath").GetString() ?? throw new InvalidOperationException("AUTOMATION_MARKER_PATH_REQUIRED"));
|
||||
}
|
||||
private static void RequireGate(Config value)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal) || !string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_LAUNCHER_MODE"), "NextPilot", StringComparison.Ordinal) || !File.Exists(value.FixtureMarkerPath)) throw new InvalidOperationException("AUTOMATION_GATE_REJECTED");
|
||||
}
|
||||
private static bool SamePath(string left, string right) => string.Equals(Path.GetFullPath(left), Path.GetFullPath(right), StringComparison.OrdinalIgnoreCase);
|
||||
private static void Write(bool completed, string? error)
|
||||
{
|
||||
if (config is null) return;
|
||||
var first = runs.ElementAtOrDefault(0); var second = runs.ElementAtOrDefault(1);
|
||||
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = completed, Prepared = prepared, FunctionalEvidenceCompleted = completed, ErrorCode = error, DbmodBefore = dbmodBaseline, DbmodAfterCommand1 = first?.DbmodAfter, DbmodAfterCommand2 = second?.DbmodAfter, Baseline = baseline, AfterCommand1 = first?.After, AfterCommand2 = second?.After, CompletedCommands = runs.Where(x => x.Ended).Select(x => "PXP-OA").ToArray(), 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 = first?.Candidates.OrderBy(x => x, StringComparer.Ordinal).FirstOrDefault(), ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, CommandsCompleted = runs.Count == 2 && runs.All(x => x.Ended), InspectorsCompleted = baseline is not null && first?.After is not null && second?.After is not null, EvidenceFlushed = true, CurrentState = terminal ? (completed ? "Completed" : "Failed") : "Prepared", StateTransitions = transitions };
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!);
|
||||
var temp = config.EvidencePath + ".tmp"; File.WriteAllText(temp, JsonSerializer.Serialize(evidence)); File.Move(temp, config.EvidencePath, true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user