Fix/new entry module logging - #3568
Conversation
The C# session manager's logging never reached any destination: the only caller of ScriptOutput.ConfigureLogging() was _setup_output(), gated on EXEC_PARAMS.first_load, which is always False under the C# loader because reading it creates the output window it tests for. Route logging from the loader as soon as the runtime assembly is available, and have the loader state first_load outright via the entry script's runtime configs rather than inferring it. Session timings also crossed a boundary they could not survive: entry scripts run with clean engines, so preload and postload never share module state. Carry them in the environment dictionary instead, restoring the load-time report and the startup output window's self-destruct.
Reloading pyRevit from a command killed the command that asked for it: the Reload button failed with "AttributeError: 'NoneType' object has no attribute 'Add'" while recording its result. Since the session load was split into preload/postload entry scripts, the Python that clears cached engines no longer runs in the calling command's engine. It protects EXEC_PARAMS.engine_id, which now names the entry script's engine, while the caller sits in an engine keyed under its own extension — so ClearEngines shut down an engine suspended mid-execution, nulling its script runtime builtin and disposing the output stream it was going to resume into. Track which engines are on the call stack and skip those, both when clearing the cache and when a clean run replaces a cached engine under the same key. The depths live in the AppDomain next to the engine cache so they survive the assembly boundary a reload crosses, and are counted so nested runs unwind in order. This covers every path that loads a session from inside a running command, not just the Reload button.
There was a problem hiding this comment.
PR Summary:
- Fix 1 (logging): The C# session loader now wires up NLog → runtime logging itself (
ConfigureRuntimeLogging), instead of relying on Python's first-load-only_setup_output(). The brokenfirst_loadinference (checkingwindow_handlebuilt the window it tested for, so it always answered "not first load") is replaced by an explicit__sessionfirstload__builtin threaded throughScriptRuntimeConfigs.Variables. Load timings now travel through the AppDomain env-var dict since preload and postload run in separate engines. - Fix 2 (reload crash): Tracks mid-execution engines via a depth-counted
ActiveEngineDict(EnterEngine/ExitEnginein atry/finallyaroundExecuteManagedScript).ClearEnginesandSetCachedEnginenow skip active engines, so a paused command's engine (e.g. Reload's) survives a session reload instead of being shut down out from under it.
Review Summary:
Both fixes are well-reasoned and the core logic is sound. I verified the ActiveEngineDict depth bookkeeping unwinds correctly for nested executions, confirmed EnvDictionary.Seed merges (not replaces) so env-var timing state survives across preload/postload engines, and traced the __sessionfirstload__ flow through ScriptRuntimeConfigs.Variables → engine SetupBuiltins → EXEC_PARAMS.first_load. The old first_load window-handle inference bug and the excludeEngine mismatch on reload are both correctly addressed. Two minor follow-ups noted below (one convention, one defensive). No blocking issues found.
Suggestions
|
|
||
| if (configureMethod == null) | ||
| { | ||
| Debug.WriteLine( |
There was a problem hiding this comment.
The repository convention for early-startup diagnostics in SessionManagerService / EnvDictionarySeeder is System.Diagnostics.Trace.WriteLine(...) (see EnvDictionarySeeder.ReadIPYVersion and ExecuteExtensionStartupScript). The two diagnostic lines added here use Debug.WriteLine instead. Debug.WriteLine is compiled out under RELEASE builds, so these messages — which are exactly the kind of "logging infrastructure not ready yet" trace the review guidelines call out — vanish in a signed/release installer where they are most needed when troubleshooting why loader logs never appear.
Use Trace.WriteLine for consistency with the surrounding early-startup diagnostics and to keep the messages in release builds:
Trace.WriteLine(
"pyRevit: ScriptOutput.ConfigureLogging not found; loader logging is discarded.");and
Trace.WriteLine($"pyRevit: failed to route loader logging into the runtime: {ex}");actions
Feedback: Rate this comment to help me improve future code reviews:
- 👍 Good - Helpful and accurate
- 👎 Poor - Wrong, unclear, or unhelpful
- Skip if you don't have any strong opinions either way.
| var activeDict = ActiveEngineDict; | ||
| int depth; | ||
| activeDict.TryGetValue(engineTypeId, out depth); | ||
| activeDict[engineTypeId] = depth + 1; |
There was a problem hiding this comment.
ActiveEngineDict / EnterEngine / ExitEngine mutate a plain Dictionary<string, int> with no locking. Today every call site (command execute, reload entry scripts) runs on the Revit main thread, so this is safe. However the existing ClearEngines already documents a single-handler concurrency caveat in ScriptExecutor.Initialize, and RequestExecuteScript exists for off-main-thread execution. If a future change ever drives ExecuteScript from a non-main thread (or nests an ExternalEvent raise that re-enters ExecuteManagedScript on the idle handler while a main-thread command is still in its try block), the depth++/depth-- bookkeeping here will race and can leave an engine permanently marked active (so it never gets shut down) or permanently removable (re-introducing the original crash).
Consider either a brief note that these helpers are main-thread-only (like the one on ScriptExecutor.Initialize), or a lock around the read-modify-write in EnterEngine/ExitEngine and the enumeration in ClearEngines so the invariant is preserved if the threading assumptions change.
actions
Feedback: Rate this comment to help me improve future code reviews:
- 👍 Good - Helpful and accurate
- 👎 Poor - Wrong, unclear, or unhelpful
- Skip if you don't have any strong opinions either way.
There was a problem hiding this comment.
Pull request overview
This PR fixes two defects in the new C# session loader's Python entry module (perform_preload/perform_postload), which replaced the old single-function load_session flow.
-
Loader logs never reached
runtime.logor the output window.ScriptOutput.ConfigureLogging()was only invoked from Python's_setup_output(), which only ran on first load and was gated by afirst_loadcheck that could never be true under the C# loader. The fix has the C#SessionManagerServicecallConfigureLogging()directly once the runtime assembly is available, and passes an explicitfirstLoadflag to the entry scripts (exposed as the__sessionfirstload__builtin) instead of inferring it from the output-window handle. Load timings are now carried across the two separate entry-script engines via new environment-dictionary keys. -
Reloading pyRevit from a button crashed the button. During a reload, cached engines are shut down except the one being skipped; that skip targeted the entry script's engine, not the still-suspended caller's engine, so the caller resumed into a disposed runtime. The fix ref-counts engines that are mid-execution (
ActiveEngineDict+Enter/ExitEngine) and never shuts one of those down.
Changes:
- Wire up runtime logging from the C# loader and pass an explicit first-load/reload flag to entry scripts.
- Carry session-start and output-setup timings through the environment dictionary instead of engine-module globals.
- Track in-flight engines by depth and suppress their shutdown during a session reload.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
pyrevitlib/pyrevit/loader/sessionmgr.py |
Replaces module-level timing/output state with env-var-carried timings; always self-destructs the session output singleton. |
pyrevitlib/pyrevit/coreutils/envvars.py |
Adds SESSIONSTARTTIME/OUTPUTSETUPTIME env-var keys. |
pyrevitlib/pyrevit/__init__.py |
first_load now reads the __sessionfirstload__ builtin, falling back to the output-window inference. |
dev/pyRevitLoader/.../SessionManagerService.cs |
Adds ConfigureRuntimeLogging(), plumbs firstLoad into entry scripts, and publishes __sessionfirstload__ via Variables. |
dev/pyRevitLabs.PyRevit.Runtime/ScriptExecutor.cs |
Wraps script execution in EnterEngine/ExitEngine (try/finally). |
dev/pyRevitLabs.PyRevit.Runtime/ScriptEngineManager.cs |
Adds ActiveEngineDict depth tracking and skips active engines in ClearEngines/SetCachedEngine. |
dev/pyRevitLabs.PyRevit.Runtime/EnvVariables.cs |
Adds the ActiveEngines AppDomain storage key. |
I reviewed the timing computation (Timer.start is a time.time() value, so time.time() - starttime is correct), the first_load builtin pattern (consistent with existing __cachedengine__/__scriptruntime__ properties), the firstLoad plumbing (LoadSession() reload passes firstLoad: false), the idempotent ConfigureLogging guard, and the reload engine-lifecycle scenario (depth counting correctly handles a caller and entry script sharing an engine TypeId). I did not find concrete defects. The changes are subtle and touch core session-loading, engine-lifecycle, and logging behavior, so final human verification in a running Revit session is warranted.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Two fixes to the new C# session loader's entry module.
1. C# loader logs never reached
runtime.logor the output window.Logging gets wired up by
ScriptOutput.ConfigureLogging(), which was only called from Python's_setup_output(), and only on first load. Under the C# loader that gate is never true — checkingfirst_loadbuilds the output window it's testing for, so it always answers "not first load."Fix: the loader wires up logging itself, as soon as the runtime assembly is available. It also tells the entry script directly whether this is a startup or a reload, instead of leaving it to guess. Load timings now travel through the environment dictionary, since preload and postload each run in their own engine and can't share variables.
2. Reloading pyRevit from a button crashed that button.
Reload died with
AttributeError: 'NoneType' object has no attribute 'Add'as soon as the session came back. On reload pyRevit shuts down cached script engines, skipping one so it doesn't kill the caller. But the load now runs in its own entry script, so the engine being skipped was the entry script's — not Reload's. Reload's engine was shut down while Reload was still paused inside it, and when it resumed, its script runtime and output stream were gone.Fix: track which engines are actually mid-execution and never shut one of those down.