Skip to content

Conversation

RobertGlobant20
Copy link
Contributor

Purpose

Fix for installing Dynamo packages using the DynamoRevit version path (e.g. C:\Users<user>\AppData\Roaming\Dynamo\Dynamo Revit\27.0\packages)
After providing the context to GitHub Copilot for updating the PathManager lazy loading singleton for changing the PathManagerParams in the constructor (using HostAnalyticsInfo.HostVersion), the next alternatives were provided by Copilot:

  1. Set Before First Access (Recommended) - Adding a static Initialize method Pass via IPathResolver
  2. Modify the Lazy Initialization Logic
  3. Set Static Properties Before First Use
  4. Recreate the Singleton (Not Recommended)

In this PR is implemented the first one with some minor changes based in GitHub Copilot suggestions.

Declarations

Check these if you believe they are true

Release Notes

Fix for installing Dynamo packages using the DynamoRevit version path (e.g. C:\Users<user>\AppData\Roaming\Dynamo\Dynamo Revit\27.0\packages)

Reviewers

@QilongTang @zeusongit

FYIs

After providing the context to GitHub Copilot for updating the PathManager lazy loading singleton and updating the PathManagerParams in the constructor, the next alternatives were provided by Copilot:

Set Before First Access (Recommended) - Adding a static Initialize method
Pass via IPathResolver
Modify the Lazy Initialization Logic
Set Static Properties Before First Use
Recreate the Singleton (Not Recommended)

In this PR is implemented the first one with some minor changes based in GitHub Copilot suggestions.
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the ticket for this pull request: https://jira.autodesk.com/browse/DYN-9348

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes installing Dynamo packages using the DynamoRevit version-specific path by implementing a singleton initialization pattern for PathManager. The fix allows the PathManager to be initialized with host version information before first access, ensuring the correct package path is used based on the host's major and minor version numbers.

Key changes:

  • Added static initialization method for PathManager singleton with version parameters
  • Modified DynamoModel constructor to initialize PathManager with host version information
  • Replaced immediate lazy initialization with deferred initialization pattern

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/DynamoCore/Models/DynamoModel.cs Added initialization logic to set PathManager with host version from configuration
src/DynamoCore/Configuration/PathManager.cs Refactored singleton pattern to support initialization with version parameters before first access

Comment on lines 62 to 66
if (!isInitialized)
{
lazy = new Lazy<PathManager>(() => new PathManager(parameters));
isInitialized = true;
}
Copy link

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Initialize method is not thread-safe. Multiple threads could simultaneously check isInitialized as false and both execute the initialization block. Consider using a lock or making this method thread-safe with double-checked locking pattern.

Copilot uses AI. Check for mistakes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think copilot is bringing this up because one of the main use cases of Lazy<T> is to make initialization thread safe when multiple threads might first initialize an object.

Because the pathmanager was made static - a bad design decision IMO - I imagine that making it lazy tries to be defensive, but the path manager itself is not thread safe AFAIK?

Anyway - my gut feeling is:

  1. This PR adds minimal complexity to an already hard to reason about piece of Dynamo, so I am okay with that.
  2. It would be nice to rip this singleton out and rethink it.
  3. The one thing I will add about alternatives is - should we in DynamoCore really be in control of these paths? It might make sense allow clients to override this by using a new member in the IPathResolver interface.

@QilongTang - am I remembering correctly that somehow Civil3d worked around this issue and put packages wherever they wanted? How did they do that?

Comment on lines 73 to 78
if (lazy == null)
{
// Fallback to default if not initialized
lazy = new Lazy<PathManager>(() => new PathManager(new PathManagerParams()));
isInitialized = true;
}
Copy link

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Instance property getter is not thread-safe. Multiple threads could simultaneously check lazy == null and both execute the fallback initialization. This could lead to race conditions and inconsistent state.

Copilot uses AI. Check for mistakes.

@RobertGlobant20
Copy link
Contributor Author

@QilongTang this one was the best solution that Copilot provided for setting the PathManagerParams in the PathManager constructor, I tested the fix in a different branch (before .NET 10 changes) and Revit Preview Release (2025) - using latest changes in DynamoRevit repo.
Please let me know what do you think of this solution (This one implied to modify a little bit the Singleton), also during my testing I noticed that all the packages now will be installed in the next path C:\Users\<user>\AppData\Roaming\Dynamo\Dynamo Revit\27.0\packages (due that DynamoRevit jumped from 3.5.0 to 27.0.0).
image

lazy =
new Lazy<PathManager>
(() => new PathManager(new PathManagerParams()));
private static Lazy<PathManager> lazy;
Copy link
Member

@mjkkirschner mjkkirschner Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this implementation Lazy the docs say:
Use lazy initialization to defer the creation of a large or resource-intensive object, or the execution of a resource-intensive task, particularly when such creation or execution might not occur during the lifetime of the program.

I think it's unlikely any of our clients will not need a path manager.

If it was to make initialization thread safe, well then, this PR breaks that invariant, so whats the point?

@mjkkirschner
Copy link
Member

this PR:

  1. should have tests
  2. we should probably evaluate all current clients to see if this will break something for them before our release - OR give them a way to override as suggested above.

@mjkkirschner
Copy link
Member

@RobertGlobant20 can you explain why you need to introduce isInitialized ?

Copy link
Contributor

@benglin benglin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @RobertGlobant20, after much debugging and considering different options, here are my thoughts and recommendations. Please let me know what you think about them.

The indeterministic nature of PathManager initialization causes it to be initialized with empty PathManagerParams() if PathManager.Instance is accessed before host applications (Revit, Civil3D) have a chance to supply DynamoModel with their desired configurations.

Note that the default empty PathManagerParams() does initialize PathManager with sensible defaults, but these don't necessarily match what the host application has in mind. For example, running Dynamo in Revit gives us the defaults shown below (note that this is without your sample path fix in PR #16578, so we still see ProgramData below):

Path Value
BackupDirectory %AppData%\Dynamo\Dynamo Core\backup
DefaultPackagesDirectory %AppData%\Dynamo\Dynamo Core\4.0\packages
DefaultUserDefinitions %AppData%\Dynamo\Dynamo Core\4.0\definitions
LogDirectory %AppData%\Dynamo\Dynamo Core\4.0\Logs
PreferenceFilePath %AppData%\Dynamo\Dynamo Core\4.0\DynamoSettings.xml
PythonTemplateFilePath %AppData%\Dynamo\Dynamo Core\4.0\PythonTemplate.py
UserDataDirectory %AppData%\Dynamo\Dynamo Core\4.0
CommonDataDirectory %ProgramData%\Dynamo\Dynamo Core\4.0
DefaultTemplatesDirectory %ProgramData%\Dynamo\Dynamo Core\templates\en-US
SamplesDirectory %ProgramData%\Dynamo\Dynamo Core\samples\en-US
DynamoCoreDirectory %UserProfile%\Documents\dev\Dynamo\bin\AnyCPU\Debug

Tweaks Needed

I agree with @mjkkirschner 100% that the singleton static PathManager is a bad idea - it needs a design overhaul, and I'd opt to rip this singleton out in a heartbeat. However, we are too close to the release date for that. So here's an alternative I'm proposing to keep the risk to a minimum.

This PR is quite close to the "ideal" way, and is frankly the better approach compared to PR #16460. It just needs a few tweaks:

  1. Remove the bool PathManager.isInitialized variable - checking if (lazy == null) is sufficient.

  2. As Copilot suggested, use a lock for thread safety:

    private static readonly object lockObject = new object();
    
    public static void Initialize(PathManagerParams parameters)
    {
        lock (lockObject)
        {
            if (lazy != null)
            {
                // Or do we want to reset the existing instance? See below for discussions.
                throw new InvalidOperationException("PathManager has already been initialized.");
            }
    
            lazy = new Lazy<PathManager>(() => new PathManager(parameters));
        }
    }
    
    public static PathManager Instance
    {
        get
        {
            if (lazy == null)
            {
                lock (lockObject)
                {
                    if (lazy == null)
                    {
                        // Fallback to default if not initialized
                        lazy = new Lazy<PathManager>(() => new PathManager(new PathManagerParams()));
                    }
                }
            }
    
            return lazy.Value;
        }
    }
  3. In DynamoModel.CreatePathManager(), we will call your new PathManager.Initialize() method:

    internal PathManager CreatePathManager(IStartConfiguration config)
    {
        var pathManagerParams = new PathManagerParams();
    
        var version = config.HostAnalyticsInfo.HostVersion;
        if (version != null)
        {
            // Use host versions if provided
            pathManagerParams.MajorFileVersion = version.Major;
            pathManagerParams.MinorFileVersion = version.Minor;
        }
    
        Dynamo.Core.PathManager.Initialize(pathManagerParams);
    
        // Existing code...
        if (!config.StartInTestMode) { }
    }
  4. After AssignHostPathAndIPathResolver is called with the host path, the paths will be as shown below, which are quite sensible (again, this is without your sample path fix in PR #16578, so we still see ProgramData below):

    Path Value
    BackupDirectory %AppData%\Dynamo\Dynamo Revit\backup
    DefaultBackupDirectory %AppData%\Dynamo\Dynamo Revit\backup
    DefaultPackagesDirectory %AppData%\Dynamo\Dynamo Revit\4.0\packages
    DefaultUserDefinitions %AppData%\Dynamo\Dynamo Revit\4.0\definitions
    LogDirectory %AppData%\Dynamo\Dynamo Revit\4.0\Logs
    PreferenceFilePath %AppData%\Dynamo\Dynamo Revit\4.0\DynamoSettings.xml
    PythonTemplateFilePath %AppData%\Dynamo\Dynamo Revit\4.0\PythonTemplate.py
    UserDataDirectory %AppData%\Dynamo\Dynamo Revit\4.0
    CommonDataDirectory %ProgramData%\Autodesk\RVT 2027\Dynamo\4.0
    DefaultTemplatesDirectory %ProgramData%\Autodesk\RVT 2027\Dynamo\templates\en-US
    SamplesDirectory %ProgramData%\Autodesk\RVT 2027\Dynamo\samples\en-US
    DynamoCoreDirectory %UserProfile%\Documents\dev\Dynamo\bin\AnyCPU\Debug
    HostApplicationDirectory %UserProfile%\Documents\dev\DynamoRevit\bin\NET100\Debug\Revit
  5. Finally, EnsureDirectoryExistence will be called to actually create these directories, so there won't be any race conditions - the paths will have the correct values when directories are created.

Final Thoughts

  • We still need to remove the IPathResolver.CommonDataRootFolder property as we move things to AppData. Both Revit and Civil3D are providing this value right now. If we'd like to keep it for compatibility reasons, we may want to mark it as obsolete and ignore its value for now.

  • The above Initialize approach will likely not make things better for Civil3D. If I'm reading it correctly, they force create a PathManager before they call AcDynamoModel.Start, which instantiates our DynamoModel, so we lose the chance to properly call Initialize with the right data. We may want to consider making Initialize override and recreate a new instance of PathManager. Crude, I know, but it would make things work for Civil3D.

  • One step closer, great work! Please help close out PR #16460 🙂

@benglin benglin mentioned this pull request Oct 9, 2025
3 tasks
Removed the isInitialized flag and added a lockObject, also removed the code from DynamoModel constructor and moved to the CreatePathManager method.
@RobertGlobant20
Copy link
Contributor Author

@benglin the PathManager Singleton implementation in the last commit was as suggested only I have to initialize inside the if(version != null) otherwise DynamoSandbox was crashing (due that version was null).

RobertGlobant20 and others added 2 commits October 13, 2025 10:24
For debugging purposes (when DynamoRevit is reading the info from Dynamo.config) this fix is needed otherwise when DynamoRevit calls GetDynamoPath method if the DynamoRevitVersion vs DynamoVersion doesn't match is returning null. Then with this fix DynamoPath will be always found and the Dynamo button will be shown in Revit.
@RobertGlobant20
Copy link
Contributor Author

GIF showing the expected behavior after installing a package in Revit.
Revit_ygI2htSXDH

}
}

public static PathManager Instance
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments for public property please

Copy link
Contributor

@QilongTang QilongTang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one comment @benglin I think this PR is pending your another look

@QilongTang QilongTang changed the title DYN-9348-Revit-PackagePath Suggested Option 1 DYN-9348-Revit-PackagePath Oct 14, 2025
@QilongTang QilongTang changed the title DYN-9348-Revit-PackagePath DYN-9348 Integrator PackagePath following Integration version Oct 14, 2025
@QilongTang
Copy link
Contributor

@RobertGlobant20 Any tests you can add in this PR?

Adding a test which verifies that the PathManager singleton instance is created with the expected properties (when using empty parameters).
@RobertGlobant20
Copy link
Contributor Author

@RobertGlobant20 Any tests you can add in this PR?
I've added a unit test in the next commit: aca9bbf
As I told you previously I haven't found a way to add a test which validates the PathManager singleton when we are passing parameters to the PathManager constructor (the HostVersion) due that is a specific flow when using a host like Revit (all the unit tests that we have right now first create the PathManager-empty parameters and then DynamoModel), so this test is only validating the case when using DynamoSandbox, please let me know if you think needs any change.

Copy link
Contributor

@benglin benglin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for accommodating the changes, @RobertGlobant20, good work as always! LGTM! 🚀

@QilongTang QilongTang merged commit b88d30b into DynamoDS:master Oct 17, 2025
26 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants