test: add controlled Plant 3D 2026 runtime smoke mode

This commit is contained in:
codex
2026-07-31 12:55:22 +03:00
parent dcf95d7e3a
commit 142b5f8b93
4 changed files with 265 additions and 27 deletions
@@ -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
}
@@ -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.
@@ -1,3 +1,4 @@
using System.Diagnostics;
using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime; using Autodesk.AutoCAD.Runtime;
@@ -9,9 +10,19 @@ namespace PRecAnnot.Plant3D2026;
public sealed class EntryPoint : IExtensionApplication 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")] [CommandMethod("PHP_REC_ANNOT")]
public void PrimaryCommand() => RunDiagnostic("PHP_REC_ANNOT"); public void PrimaryCommand() => RunDiagnostic("PHP_REC_ANNOT");
@@ -24,41 +35,123 @@ public sealed class EntryPoint : IExtensionApplication
private static void RunDiagnostic(string commandName) 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 try
{ {
Document? document = AcadApplication.DocumentManager.MdiActiveDocument; document = AcadApplication.DocumentManager.MdiActiveDocument;
if (document is null) var project = PlantApplication.CurrentProject;
return; var details = new Dictionary<string, object?>
Editor editor = document.Editor;
if (PlantApplication.CurrentProject is null)
{ {
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; 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) catch (System.Exception exception)
{ {
try { AcadApplication.DocumentManager.MdiActiveDocument?.Editor.WriteMessage($"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}"); } RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, "Failed", exception, document);
catch { } 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<string, object?>? details, long duration, System.Exception? exception = null)
{
var eventDetails = new Dictionary<string, object?>
{
["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 internal static class RibbonIntegration
{ {
private const string TabId = "PRecAnnot.Plant3D2026"; 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 const string Command = "PHP_REC_ANNOT ";
private static bool deferred;
public static void Register() public static void Register()
{ {
RuntimeDiagnostics.Write("RibbonInitializationStarted");
try try
{ {
RibbonControl? ribbon = ComponentManager.Ribbon; if (ComponentManager.Ribbon is null)
if (ribbon is null) return; {
if (!deferred)
{
ComponentManager.ItemInitialized += OnItemInitialized;
deferred = true;
}
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Deferred");
return;
}
RegisterNow();
}
catch (System.Exception exception)
{
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)); RibbonTab? tab = ribbon.Tabs.FirstOrDefault(item => string.Equals(item.Id, TabId, StringComparison.OrdinalIgnoreCase));
if (tab is null) if (tab is null)
{ {
@@ -66,15 +159,24 @@ internal static class RibbonIntegration
ribbon.Tabs.Add(tab); ribbon.Tabs.Add(tab);
} }
if (tab.Panels.Any(panel => string.Equals(panel.Source?.Title, "Аннотации", StringComparison.OrdinalIgnoreCase))) return; RibbonPanel? panel = tab.Panels.FirstOrDefault(item => string.Equals(item.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase));
if (panel is not null)
RibbonPanelSource source = new() { Title = "Аннотации" };
source.Items.Add(new RibbonButton { Text = "Аннотировать", ShowText = true, CommandParameter = Command });
tab.Panels.Add(new RibbonPanel { Source = source });
}
catch
{ {
// Ribbon is optional; command-line registration remains available. 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;
} }
} }
@@ -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<string, object?>? details = null)
{
try
{
var payload = new Dictionary<string, object?>(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(); }
}