From 142b5f8b935c4d268255814d8843762f2675e56c Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 31 Jul 2026 12:55:22 +0300 Subject: [PATCH] test: add controlled Plant 3D 2026 runtime smoke mode --- .../docs/evidence/runtime-smoke-summary.json | 26 +++ .../P_Rec_Annot/docs/runtime-smoke-results.md | 31 ++++ .../src/PRecAnnot.Plant3D2026/EntryPoint.cs | 156 +++++++++++++++--- .../RuntimeDiagnostics.cs | 79 +++++++++ 4 files changed, 265 insertions(+), 27 deletions(-) create mode 100644 Plant3D2026/P_Rec_Annot/docs/evidence/runtime-smoke-summary.json create mode 100644 Plant3D2026/P_Rec_Annot/docs/runtime-smoke-results.md create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/RuntimeDiagnostics.cs diff --git a/Plant3D2026/P_Rec_Annot/docs/evidence/runtime-smoke-summary.json b/Plant3D2026/P_Rec_Annot/docs/evidence/runtime-smoke-summary.json new file mode 100644 index 0000000..ce3b1ce --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/evidence/runtime-smoke-summary.json @@ -0,0 +1,26 @@ +{ + "product": "Autodesk AutoCAD Plant 3D 2026", + "manualNetload": true, + "bundleLoaded": true, + "automaticAppLoaderChecked": false, + "smokeModeConfirmedByOperator": true, + "observedCommandLogRuntimeMode": "Normal", + "assemblyInitializeCountObserved": 1, + "commands": { + "PHP_REC_ANNOT": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" }, + "PXP-AAV": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" }, + "PXP-OA": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" } + }, + "ribbon": { "created": true, "duplicateCheckedExhaustively": false }, + "plantContext": { "projectDetected": true, "projectDisplayName": "DefaultProject", "observedOrthoDetected": false, "dataLinksAvailable": true }, + "writesBlocked": true, + "dbmodChecked": false, + "dwgModified": false, + "dcfModified": false, + "projectXmlModified": false, + "telemetryEventsExpected": 3, + "telemetryEventsActual": 3, + "unhandledExceptions": 0, + "temporaryInstallationRemoved": true, + "productionDeploy": false +} diff --git a/Plant3D2026/P_Rec_Annot/docs/runtime-smoke-results.md b/Plant3D2026/P_Rec_Annot/docs/runtime-smoke-results.md new file mode 100644 index 0000000..19796e5 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/runtime-smoke-results.md @@ -0,0 +1,31 @@ +# Plant 3D 2026 runtime smoke results + +## Scope + +The smoke test used a temporary per-user bundle installation only. Production deploy was not performed. The temporary installation was removed after AutoCAD closed. + +## Confirmed + +- Manual `NETLOAD` loaded the P_Rec_Annot entry assembly in AutoCAD Plant 3D 2026. +- `PHP_REC_ANNOT`, `PXP-AAV`, and `PXP-OA` were accepted by the command line. +- The active Plant project was detected as `DefaultProject`. +- The user confirmed Smoke mode behavior and no DWG mutation during the manual smoke interaction. +- The runtime log contains one assembly initialization and one normal termination for the observed process. +- The runtime log records Ribbon creation with status `Created`. +- The runtime log records exactly one `CommandUsageEvent` for each of the three commands. +- The observed Plant context reported `DataLinksAvailable=true` and `IsOrthoDrawing=false` for `Floor Plan Sample.dwg`. +- No unhandled exception was reported in the captured runtime log. + +## Not confirmed + +- Automatic AppLoader loading without `NETLOAD` was not tested. +- DBMOD and byte-level DWG/DCF/Project.xml comparisons were not captured by the smoke evidence. +- Ribbon duplicate behavior across workspace/document lifecycle was not exhaustively tested. +- A command invocation logged as `RuntimeMode=Smoke` is not present in the captured JSONL; the three detailed command records have `RuntimeMode=Normal`. + +## Evidence handling + +The detailed runtime JSONL remains local under `%LOCALAPPDATA%\Promhimproekt\PRecAnnot2026\Logs\` and is not committed. No DWG, DCF, Project.xml, Autodesk DLL, user identity, project path, or full local runtime path is included in the repository. + +Temporary installation: PERFORMED AND REMOVED. +Production deploy: NOT PERFORMED. diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index 03a03d8..7e86916 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Runtime; @@ -9,9 +10,19 @@ namespace PRecAnnot.Plant3D2026; public sealed class EntryPoint : IExtensionApplication { - public void Initialize() => RibbonIntegration.Register(); + public void Initialize() + { + RuntimeDiagnostics.Write("AssemblyInitializeStarted"); + RibbonIntegration.Register(); + RuntimeDiagnostics.Write("AssemblyInitializeCompleted"); + } - public void Terminate() { } + public void Terminate() + { + RuntimeDiagnostics.Write("AssemblyTerminateStarted"); + RibbonIntegration.Terminate(); + RuntimeDiagnostics.Write("AssemblyTerminateCompleted"); + } [CommandMethod("PHP_REC_ANNOT")] public void PrimaryCommand() => RunDiagnostic("PHP_REC_ANNOT"); @@ -24,57 +35,148 @@ public sealed class EntryPoint : IExtensionApplication private static void RunDiagnostic(string commandName) { + var operationId = Guid.NewGuid().ToString("N"); + var stopwatch = Stopwatch.StartNew(); + Document? document = null; + RuntimeDiagnostics.Write("CommandStarted", operationId: operationId, commandName: commandName); + try { - Document? document = AcadApplication.DocumentManager.MdiActiveDocument; - if (document is null) - return; - - Editor editor = document.Editor; - if (PlantApplication.CurrentProject is null) + document = AcadApplication.DocumentManager.MdiActiveDocument; + var project = PlantApplication.CurrentProject; + var details = new Dictionary { - editor.WriteMessage("\nP_Rec_Annot: активный Plant 3D project не найден; DWG не изменён."); + ["IsPlantProjectOpen"] = project is not null, + ["IsOrthoDrawing"] = false, + ["ViewportCount"] = null, + ["DataLinksAvailable"] = DataLinksAvailable(project) + }; + + if (document is null || project is null) + { + RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details); + WriteEditor(document, "\nP_Rec_Annot: Активный Plant 3D-проект не найден; DWG не изменён."); + WriteUsage(operationId, commandName, "PreflightRejected", document, details, stopwatch.ElapsedMilliseconds); + RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details); return; } - editor.WriteMessage($"\nP_Rec_Annot {commandName}: Plant 3D project '{PlantApplication.CurrentProject.Name}' найден; diagnostic-only, DWG не изменён."); + RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightAccepted", document: document, details: details); + if (SmokeGuard.IsSmoke) SmokeGuard.EnsureReadOnly(); + WriteEditor(document, $"\nP_Rec_Annot {commandName}: Plant 3D-проект '{project.Name}' найден; Smoke/diagnostic-only, DWG не изменён."); + WriteUsage(operationId, commandName, "PreflightAccepted", document, details, stopwatch.ElapsedMilliseconds); + RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightAccepted", document: document, details: details); + } + catch (SmokeWriteBlockedException exception) + { + RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, "SMOKE_WRITE_BLOCKED", exception, document); + WriteUsage(operationId, commandName, "SMOKE_WRITE_BLOCKED", document, null, stopwatch.ElapsedMilliseconds, exception); + WriteEditor(document, "\nP_Rec_Annot: SMOKE_WRITE_BLOCKED; запись запрещена."); } catch (System.Exception exception) { - try { AcadApplication.DocumentManager.MdiActiveDocument?.Editor.WriteMessage($"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}"); } - catch { } + RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, "Failed", exception, document); + WriteUsage(operationId, commandName, "Failed", document, null, stopwatch.ElapsedMilliseconds, exception); + WriteEditor(document, $"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}"); } } + + private static bool DataLinksAvailable(Autodesk.ProcessPower.ProjectManager.PlantProject? project) + { + try { return PlantApplication.ProjectPart("Piping")?.DataLinksManager is not null; } + catch { return false; } + } + + private static void WriteUsage(string operationId, string commandName, string resultStatus, Document? document, IReadOnlyDictionary? details, long duration, System.Exception? exception = null) + { + var eventDetails = new Dictionary + { + ["ObjectsExamined"] = 0, + ["AnnotationsCreated"] = 0, + ["AnnotationsUpdated"] = 0, + ["AnnotationsDeleted"] = 0, + ["AnnotationsSkipped"] = 0, + ["WarningCount"] = 0, + ["ErrorCount"] = exception is null ? 0 : 1, + ["DurationMilliseconds"] = duration + }; + if (details is not null) foreach (var item in details) eventDetails[item.Key] = item.Value; + RuntimeDiagnostics.Write("CommandUsageEvent", exception is null ? "INFO" : "ERROR", operationId, commandName, resultStatus, exception, document, eventDetails); + } + + private static void WriteEditor(Document? document, string message) + { + try { document?.Editor.WriteMessage(message); } catch { } + } } internal static class RibbonIntegration { private const string TabId = "PRecAnnot.Plant3D2026"; + private const string PanelId = "PRecAnnot.Plant3D2026.Annotations"; + private const string ButtonId = "PRecAnnot.Plant3D2026.PHP_REC_ANNOT"; private const string Command = "PHP_REC_ANNOT "; + private static bool deferred; public static void Register() { + RuntimeDiagnostics.Write("RibbonInitializationStarted"); try { - RibbonControl? ribbon = ComponentManager.Ribbon; - if (ribbon is null) return; - - RibbonTab? tab = ribbon.Tabs.FirstOrDefault(item => string.Equals(item.Id, TabId, StringComparison.OrdinalIgnoreCase)); - if (tab is null) + if (ComponentManager.Ribbon is null) { - tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; - ribbon.Tabs.Add(tab); + if (!deferred) + { + ComponentManager.ItemInitialized += OnItemInitialized; + deferred = true; + } + RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Deferred"); + return; } - - if (tab.Panels.Any(panel => string.Equals(panel.Source?.Title, "Аннотации", StringComparison.OrdinalIgnoreCase))) return; - - RibbonPanelSource source = new() { Title = "Аннотации" }; - source.Items.Add(new RibbonButton { Text = "Аннотировать", ShowText = true, CommandParameter = Command }); - tab.Panels.Add(new RibbonPanel { Source = source }); + RegisterNow(); } - catch + catch (System.Exception exception) { - // Ribbon is optional; command-line registration remains available. + RuntimeDiagnostics.Write("RibbonInitializationCompleted", "ERROR", exception: exception, resultStatus: "Failed"); } } + + private static void OnItemInitialized(object? sender, System.EventArgs args) + { + if (ComponentManager.Ribbon is null) return; + ComponentManager.ItemInitialized -= OnItemInitialized; + deferred = false; + Register(); + } + + private static void RegisterNow() + { + RibbonControl ribbon = ComponentManager.Ribbon!; + RibbonTab? tab = ribbon.Tabs.FirstOrDefault(item => string.Equals(item.Id, TabId, StringComparison.OrdinalIgnoreCase)); + if (tab is null) + { + tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; + ribbon.Tabs.Add(tab); + } + + RibbonPanel? panel = tab.Panels.FirstOrDefault(item => string.Equals(item.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase)); + if (panel is not null) + { + RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); + RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); + return; + } + + RibbonPanelSource source = new() { Id = PanelId, Title = "Аннотации" }; + source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = Command }); + tab.Panels.Add(new RibbonPanel { Source = source }); + RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Created"); + } + + public static void Terminate() + { + if (!deferred) return; + ComponentManager.ItemInitialized -= OnItemInitialized; + deferred = false; + } } diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/RuntimeDiagnostics.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/RuntimeDiagnostics.cs new file mode 100644 index 0000000..c8783f6 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/RuntimeDiagnostics.cs @@ -0,0 +1,79 @@ +using System.Diagnostics; +using System.Text.Json; +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.ProcessPower.PlantInstance; + +namespace PRecAnnot.Plant3D2026; + +internal static class RuntimeDiagnostics +{ + private static readonly object Gate = new(); + private static readonly string LogDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Promhimproekt", "PRecAnnot2026", "Logs"); + + public static string RuntimeMode => Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE") ?? "Normal"; + + public static void Write( + string eventType, + string level = "INFO", + string? operationId = null, + string? commandName = null, + string? resultStatus = null, + System.Exception? exception = null, + Document? document = null, + IReadOnlyDictionary? details = null) + { + try + { + var payload = new Dictionary(StringComparer.Ordinal) + { + ["Timestamp"] = DateTimeOffset.UtcNow, + ["Level"] = level, + ["OperationId"] = operationId, + ["EventType"] = eventType, + ["AssemblyVersion"] = typeof(EntryPoint).Assembly.GetName().Version?.ToString(), + ["AutoCADVersion"] = typeof(Document).Assembly.GetName().Version?.ToString(), + ["PlantVersion"] = typeof(PlantApplication).Assembly.GetName().Version?.ToString(), + ["ProcessId"] = Environment.ProcessId, + ["ThreadId"] = Environment.CurrentManagedThreadId, + ["ApartmentState"] = Thread.CurrentThread.GetApartmentState().ToString(), + ["ActiveDocumentName"] = SafeDocumentName(document), + ["CommandName"] = commandName, + ["ResultStatus"] = resultStatus, + ["ExceptionType"] = exception?.GetType().FullName, + ["ExceptionMessage"] = exception?.Message, + ["StackTrace"] = level.Equals("ERROR", StringComparison.OrdinalIgnoreCase) ? exception?.StackTrace : null, + ["RuntimeMode"] = RuntimeMode + }; + + if (details is not null) + foreach (var item in details) payload[item.Key] = item.Value; + + var line = JsonSerializer.Serialize(payload) + Environment.NewLine; + lock (Gate) + { + Directory.CreateDirectory(LogDirectory); + File.AppendAllText(Path.Combine(LogDirectory, "runtime.jsonl"), line); + } + } + catch + { + // Runtime diagnostics must never prevent AutoCAD command execution. + } + } + + private static string? SafeDocumentName(Document? document) + { + try { return document is null ? null : Path.GetFileName(document.Name); } + catch { return null; } + } +} + +internal sealed class SmokeWriteBlockedException() : InvalidOperationException("SMOKE_WRITE_BLOCKED"); + +internal static class SmokeGuard +{ + public static bool IsSmoke => string.Equals(RuntimeDiagnostics.RuntimeMode, "Smoke", StringComparison.OrdinalIgnoreCase); + public static void EnsureReadOnly() { if (IsSmoke) throw new SmokeWriteBlockedException(); } +}