using System.Text.Json; using System.Reflection; using System.Security.Cryptography; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Runtime; using Autodesk.ProcessPower.PlantInstance; using Autodesk.ProcessPower.ProjectManager; 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, string ExpectedEntryAssemblySha256, bool Diagnostic); private sealed record Run(int Sequence, string? PlanId, IReadOnlyList 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 runs = []; private static readonly List transitions = []; private static readonly List commandEvents = []; private static readonly List sentinels = []; private static PxpOaDiscoveryEvidence? discovery; private static bool prepared; private static bool terminal; internal static void Prepare() { try { if (terminal) return; config = ReadConfig(); RequireGate(config); discovery = DiscoverPxpOa(config); RequireDiscovery(discovery); 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 projectXml = Path.Combine(Path.GetDirectoryName(config.FixtureMarkerPath)!, "Project.xml"); if (!File.Exists(projectXml)) throw new InvalidOperationException("AUTOMATION_PROJECT_XML_MISSING"); var project = PlantProject.LoadProject(projectXml, false, string.Empty, string.Empty); PlantApplication.SetCurrentProject(project, true); 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 RecordSentinel(string stage) { Guard(() => { if (!prepared || terminal || fixture is null || database is null) return; sentinels.Add(new(stage, fixture.Name, database.Filename, DateTimeOffset.UtcNow)); transitions.Add("Sentinel:" + stage); Write(false, null); }); } internal static void RecordPxpOa(string? planId, IReadOnlyList 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(() => { commandEvents.Add("WillStart:" + args.GlobalCommandName); 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(() => { commandEvents.Add((failed ? "Failed:" : cancelled ? "Cancelled:" : "Ended:") + args.GlobalCommandName); 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"), root.GetProperty("entryAssemblySha256").GetString() ?? throw new InvalidOperationException("AUTOMATION_ENTRY_ASSEMBLY_HASH_REQUIRED"), root.TryGetProperty("diagnostic", out var diagnostic) && diagnostic.ValueKind == JsonValueKind.True); } 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, PxpOaDiscovery = discovery, Sentinels = sentinels, CommandEvents = commandEvents }; Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!); var temp = config.EvidencePath + ".tmp"; File.WriteAllText(temp, JsonSerializer.Serialize(evidence)); File.Move(temp, config.EvidencePath, true); } private static PxpOaDiscoveryEvidence DiscoverPxpOa(Config value) { var entry = typeof(EntryPoint).Assembly; var loaded = AppDomain.CurrentDomain.GetAssemblies().Any(x => ReferenceEquals(x, entry)); var location = entry.Location; var hash = File.Exists(location) ? Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(location))) : null; var declarations = AppDomain.CurrentDomain.GetAssemblies().SelectMany(Declarations).Where(x => string.Equals(x.Name, "PXP-OA", StringComparison.OrdinalIgnoreCase)).ToArray(); var owner = declarations.SingleOrDefault(x => x.Assembly == entry); var commandClass = entry.GetCustomAttributesData().Any(x => string.Equals(x.AttributeType.FullName, "Autodesk.AutoCAD.Runtime.CommandClassAttribute", StringComparison.Ordinal) && x.ConstructorArguments.Any(a => string.Equals(a.Value as Type, typeof(EntryPoint)))); return new(owner is not null, owner?.Name, owner?.Group, owner?.Flags, owner?.Type.FullName, owner?.Assembly.GetName().Name, location, hash, loaded, commandClass, hash is not null && string.Equals(hash, value.ExpectedEntryAssemblySha256, StringComparison.OrdinalIgnoreCase), declarations.Length); } private sealed record Declaration(string Name, string? Group, string? Flags, Type Type, Assembly Assembly); private static IEnumerable Declarations(Assembly assembly) { Type[] types; try { types = assembly.GetTypes(); } catch (ReflectionTypeLoadException exception) { types = exception.Types.Where(x => x is not null).Cast().ToArray(); } foreach (var type in types) foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) foreach (var attribute in method.GetCustomAttributesData().Where(x => string.Equals(x.AttributeType.FullName, "Autodesk.AutoCAD.Runtime.CommandMethodAttribute", StringComparison.Ordinal))) { var name = attribute.ConstructorArguments.FirstOrDefault().Value as string; if (string.IsNullOrWhiteSpace(name)) continue; var group = attribute.NamedArguments.FirstOrDefault(x => x.MemberName == "GroupName").TypedValue.Value as string; var flags = attribute.NamedArguments.FirstOrDefault(x => x.MemberName == "Flags").TypedValue.Value?.ToString(); yield return new(name, group, flags, type, assembly); } } private static void RequireDiscovery(PxpOaDiscoveryEvidence value) { if (!value.AssemblyLoaded) throw new InvalidOperationException("PXP_OA_ASSEMBLY_NOT_LOADED"); if (!value.BundleHashMatched) throw new InvalidOperationException("PXP_OA_BUNDLE_HASH_MISMATCH"); if (!value.CommandClassIncluded) throw new InvalidOperationException("PXP_OA_COMMANDCLASS_NOT_REGISTERED"); if (value.DeclarationCount > 1) throw new InvalidOperationException("PXP_OA_COMMAND_COLLISION"); if (value.DeclarationCount == 0 || !value.Found) throw new InvalidOperationException("PXP_OA_COMMAND_NOT_FOUND"); if (!string.Equals(value.GlobalName, "PXP-OA", StringComparison.Ordinal)) throw new InvalidOperationException("PXP_OA_GLOBAL_NAME_MISMATCH"); } }