Skip to content

Commit 47a34c3

Browse files
committed
feat: populate AppDomain session environment dictionary (FIX 1)
The C# loader never seeded the PYREVITEnvVarsDict AppDomain key, so all pyrevit.sessioninfo / telemetry / logger APIs in scripts were crashing with NullReferenceException when EnvDictionary() tried to cast null to PythonDictionary. Changes: - EnvVariables.cs (Runtime): add static EnvDictionary.Seed() that accepts a plain Dictionary<string, object>, creates/gets the PythonDictionary internally, populates it, and stores it in AppDomain. Keeping PythonDictionary creation inside the Runtime avoids a compile-time IronPython reference in the loader (UseIronPython=false). - PyRevitConfig.cs: add 12 missing config properties read from the same INI sections as PyRevitConsts in the CLI library: LoggingLevel (verbose/debug → 0/1/2), FileLogging, AutoUpdate, OutputStyleSheet [core]; TelemetryState, TelemetryUTCTimeStamps, TelemetryFilePath, TelemetryServerUrl, TelemetryIncludeHooks, AppTelemetryState, AppTelemetryServerUrl, AppTelemetryEventFlags [telemetry]. - EnvDictionarySeeder.cs (new): builds the values dict from PyRevitConfig and UIApplication, resolves pyRevit version from the version file and IronPython version from the engine DLL, then calls EnvDictionary.Seed() via reflection (same pattern as ScriptExecutor.Initialize/ExecuteScript). - SessionManagerService.cs: call SeedEnvironmentDictionary() immediately after InitializeScriptExecutor() (which loads _runtimeAssembly) and before any extension startup script runs. Failure is logged as a warning rather than thrown to preserve graceful degradation. - ScriptMetadataParsingTest.cs: add TestLoggingLevelConfigFromIni, TestTelemetryConfigFromIni, TestFileLoggingAndAutoUpdateConfigFromIni. https://claude.ai/code/session_013HpJtXCz4WQuSaNxY2gSxK
1 parent 62dc929 commit 47a34c3

5 files changed

Lines changed: 543 additions & 1 deletion

File tree

dev/pyRevitLabs.PyRevit.Runtime/EnvVariables.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,5 +169,28 @@ public EnvDictionary()
169169
public void ResetEventHooks() {
170170
((Dictionary<string, Dictionary<string, string>>)_envData[EnvDictionaryKeys.Hooks]).Clear();
171171
}
172+
173+
/// <summary>
174+
/// Seeds the AppDomain environment dictionary with session values supplied by the C# loader.
175+
/// Called via reflection by EnvDictionarySeeder in pyRevitAssemblyBuilder (which has no
176+
/// compile-time reference to IronPython), so the PythonDictionary is created here where
177+
/// IronPython is already available.
178+
/// </summary>
179+
/// <param name="values">
180+
/// Key/value pairs to store. Keys must match the string values of <see cref="EnvDictionaryKeys"/>.
181+
/// Values must be plain CLR primitives (string, bool, int) — IronPython coerces them correctly.
182+
/// </param>
183+
public static void Seed(Dictionary<string, object> values) {
184+
var envData = AppDomain.CurrentDomain.GetData(DomainStorageKeys.EnvVarsDictKey) as PythonDictionary
185+
?? new PythonDictionary();
186+
187+
foreach (var kv in values)
188+
envData[kv.Key] = kv.Value;
189+
190+
if (!envData.Contains(EnvDictionaryKeys.Hooks))
191+
envData[EnvDictionaryKeys.Hooks] = new Dictionary<string, Dictionary<string, string>>();
192+
193+
AppDomain.CurrentDomain.SetData(DomainStorageKeys.EnvVarsDictKey, envData);
194+
}
172195
}
173196
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Reflection;
6+
using Autodesk.Revit.UI;
7+
using pyRevitExtensionParser;
8+
9+
namespace pyRevitAssemblyBuilder.SessionManager
10+
{
11+
/// <summary>
12+
/// Seeds the AppDomain environment dictionary consumed by the pyRevit Runtime.
13+
/// <para>
14+
/// The Runtime's <c>EnvDictionary</c> class reads session state (UUID, versions, telemetry
15+
/// settings, etc.) from an <c>IronPython.Runtime.PythonDictionary</c> stored in the AppDomain
16+
/// under the key <c>"PYREVITEnvVarsDict"</c>. Because this loader project has no compile-time
17+
/// reference to IronPython (<c>UseIronPython=false</c>), we delegate the actual
18+
/// <c>PythonDictionary</c> creation to the Runtime via <c>EnvDictionary.Seed()</c>, which is
19+
/// invoked here through reflection — the same pattern already used for
20+
/// <c>ScriptExecutor.Initialize()</c> and <c>ScriptExecutor.ExecuteScript()</c>.
21+
/// </para>
22+
/// </summary>
23+
internal static class EnvDictionarySeeder
24+
{
25+
// Env-dict key string values. These must match EnvDictionaryKeys in the Runtime
26+
// (dev/pyRevitLabs.PyRevit.Runtime/EnvVariables.cs). The prefix is "PYREVIT" because
27+
// PyRevitLabsConsts.ProductName = "PYREVIT".
28+
private const string KeySessionUUID = "PYREVIT_UUID";
29+
private const string KeyRevitVersion = "PYREVIT_APPVERSION";
30+
private const string KeyVersion = "PYREVIT_VERSION";
31+
private const string KeyClone = "PYREVIT_CLONE";
32+
private const string KeyIPYVersion = "PYREVIT_IPYVERSION";
33+
private const string KeyCPYVersion = "PYREVIT_CPYVERSION";
34+
private const string KeyLoggingLevel = "PYREVIT_LOGGINGLEVEL";
35+
private const string KeyFileLogging = "PYREVIT_FILELOGGING";
36+
private const string KeyTelemetryState = "PYREVIT_TELEMETRYSTATE";
37+
private const string KeyTelemetryUTC = "PYREVIT_TELEMETRYUTCTIMESTAMPS";
38+
private const string KeyTelemetryFile = "PYREVIT_TELEMETRYFILE";
39+
private const string KeyTelemetryServer = "PYREVIT_TELEMETRYSERVER";
40+
private const string KeyTelemetryHooks = "PYREVIT_TELEMETRYINCLUDEHOOKS";
41+
private const string KeyAppTelemetryState = "PYREVIT_APPTELEMETRYSTATE";
42+
private const string KeyAppTelemetryServer = "PYREVIT_APPTELEMETRYSERVER";
43+
private const string KeyAppTelemetryFlags = "PYREVIT_APPTELEMETRYEVENTFLAGS";
44+
private const string KeyAutoUpdating = "PYREVIT_AUTOUPDATE";
45+
private const string KeyOutputStyleSheet = "PYREVIT_STYLESHEET";
46+
47+
/// <summary>
48+
/// Builds the session environment dictionary and stores it in the AppDomain via a reflection
49+
/// call to <c>EnvDictionary.Seed()</c> in the Runtime assembly.
50+
/// </summary>
51+
/// <param name="uiApp">The active Revit UIApplication (provides version number).</param>
52+
/// <param name="runtimeAssembly">
53+
/// The already-loaded <c>pyRevitLabs.PyRevit.Runtime</c> assembly.
54+
/// </param>
55+
/// <param name="pyRevitRoot">
56+
/// Root directory of the pyRevit repository (used to read the version file and locate engine
57+
/// binaries). May be empty — the seeder degrades gracefully to "Unknown" where needed.
58+
/// </param>
59+
public static void Seed(UIApplication uiApp, Assembly runtimeAssembly, string pyRevitRoot)
60+
{
61+
var config = PyRevitConfig.Load();
62+
63+
var values = new Dictionary<string, object>
64+
{
65+
[KeySessionUUID] = Guid.NewGuid().ToString(),
66+
[KeyRevitVersion] = uiApp?.Application?.VersionNumber ?? string.Empty,
67+
[KeyVersion] = ReadPyRevitVersion(pyRevitRoot),
68+
[KeyClone] = "Unknown",
69+
[KeyIPYVersion] = ReadIPYVersion(pyRevitRoot),
70+
[KeyCPYVersion] = "3.12.3", // Known default for the bundled CPython engine
71+
72+
[KeyLoggingLevel] = config.LoggingLevel,
73+
[KeyFileLogging] = config.FileLogging,
74+
75+
[KeyTelemetryState] = config.TelemetryState,
76+
[KeyTelemetryUTC] = config.TelemetryUTCTimeStamps,
77+
[KeyTelemetryFile] = config.TelemetryFilePath,
78+
[KeyTelemetryServer] = config.TelemetryServerUrl,
79+
[KeyTelemetryHooks] = config.TelemetryIncludeHooks,
80+
81+
[KeyAppTelemetryState] = config.AppTelemetryState,
82+
[KeyAppTelemetryServer] = config.AppTelemetryServerUrl,
83+
[KeyAppTelemetryFlags] = config.AppTelemetryEventFlags,
84+
85+
[KeyAutoUpdating] = config.AutoUpdate,
86+
[KeyOutputStyleSheet] = config.OutputStyleSheet,
87+
};
88+
89+
// Delegate to EnvDictionary.Seed() in the Runtime, which owns PythonDictionary creation.
90+
var envDictType = runtimeAssembly.GetType("PyRevitLabs.PyRevit.Runtime.EnvDictionary")
91+
?? throw new InvalidOperationException("Cannot find type PyRevitLabs.PyRevit.Runtime.EnvDictionary in runtime assembly.");
92+
93+
var seedMethod = envDictType.GetMethod(
94+
"Seed",
95+
BindingFlags.Public | BindingFlags.Static,
96+
null,
97+
new[] { typeof(Dictionary<string, object>) },
98+
null)
99+
?? throw new InvalidOperationException("Cannot find EnvDictionary.Seed(Dictionary<string, object>) method.");
100+
101+
seedMethod.Invoke(null, new object[] { values });
102+
}
103+
104+
private static string ReadPyRevitVersion(string pyRevitRoot)
105+
{
106+
if (string.IsNullOrEmpty(pyRevitRoot))
107+
return "Unknown";
108+
109+
// pyRevit version is stored as a bare version string in pyrevitlib/pyrevit/version
110+
var versionFile = Path.Combine(pyRevitRoot, "pyrevitlib", "pyrevit", "version");
111+
if (File.Exists(versionFile))
112+
{
113+
try { return File.ReadAllText(versionFile).Trim(); }
114+
catch { /* fall through to Unknown */ }
115+
}
116+
117+
return "Unknown";
118+
}
119+
120+
private static string ReadIPYVersion(string pyRevitRoot)
121+
{
122+
// IronPython engines live under bin/ inside the repo root as well as beside this DLL.
123+
// Check both the bin/ folder of the repo and the directory of the executing assembly.
124+
var candidateDirs = new List<string>();
125+
126+
if (!string.IsNullOrEmpty(pyRevitRoot))
127+
{
128+
candidateDirs.Add(Path.Combine(pyRevitRoot, "bin", "IPY342"));
129+
candidateDirs.Add(Path.Combine(pyRevitRoot, "bin", "IPY2712PR"));
130+
candidateDirs.Add(Path.Combine(pyRevitRoot, "bin"));
131+
}
132+
133+
var selfDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
134+
if (!string.IsNullOrEmpty(selfDir))
135+
candidateDirs.Add(selfDir);
136+
137+
foreach (var dir in candidateDirs)
138+
{
139+
var dll = Path.Combine(dir, "IronPython.dll");
140+
if (!File.Exists(dll)) continue;
141+
try
142+
{
143+
var ver = AssemblyName.GetAssemblyName(dll).Version;
144+
if (ver != null) return ver.ToString();
145+
}
146+
catch { /* try next candidate */ }
147+
}
148+
149+
return "Unknown";
150+
}
151+
}
152+
}

dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/SessionManagerService.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,14 @@ public void LoadSession()
106106
stepStopwatch.Restart();
107107
InitializeScriptExecutor();
108108
_logger.Debug($"[PERF] InitializeScriptExecutor: {stepStopwatch.ElapsedMilliseconds}ms");
109-
109+
110+
// Seed the AppDomain environment dictionary. Must run after InitializeScriptExecutor()
111+
// (which loads _runtimeAssembly) and before any extension startup script (which may call
112+
// pyrevit.sessioninfo, pyrevit.telemetry, etc.).
113+
stepStopwatch.Restart();
114+
SeedEnvironmentDictionary();
115+
_logger.Debug($"[PERF] SeedEnvironmentDictionary: {stepStopwatch.ElapsedMilliseconds}ms");
116+
110117
// Get all library extensions first - they need to be available to all UI extensions
111118
stepStopwatch.Restart();
112119
var libraryExtensions = _extensionManager?.GetInstalledLibraryExtensions()?.ToList() ?? new List<ParsedExtension>();
@@ -200,6 +207,26 @@ public void LoadSession()
200207
_logger.Info($"Session loaded in {totalStopwatch.ElapsedMilliseconds}ms");
201208
}
202209

210+
private void SeedEnvironmentDictionary()
211+
{
212+
try
213+
{
214+
if (_runtimeAssembly == null)
215+
{
216+
_logger.Warning("Cannot seed environment dictionary: runtime assembly not loaded.");
217+
return;
218+
}
219+
220+
EnvDictionarySeeder.Seed(_uiApp, _runtimeAssembly, _pyRevitRoot ?? string.Empty);
221+
_logger.Debug("Session environment dictionary seeded successfully.");
222+
}
223+
catch (Exception ex)
224+
{
225+
// Non-fatal: scripts that don't rely on env vars still work.
226+
_logger.Warning($"Failed to seed environment dictionary: {ex.Message}");
227+
}
228+
}
229+
203230
private void InitializeScriptExecutor()
204231
{
205232
// Cache runtime assembly lookup - it's used by every extension

0 commit comments

Comments
 (0)