Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 91 additions & 2 deletions src/DynamoCore/Configuration/PathManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -754,20 +754,109 @@ internal void EnsureDirectoryExistence(List<Exception> exceptions)
exceptions.RemoveAll(x => x == null); // Remove all null entries.
}

/// <summary>
/// Returns true when the directory exists and contains at least one .dyn file.
/// </summary>
/// <param name="directoryPath">The template directory path to validate.</param>
internal static bool IsValidTemplatesDirectory(string directoryPath)
{
if (string.IsNullOrEmpty(directoryPath) || !Directory.Exists(directoryPath))
return false;

try
{
return Directory.EnumerateFiles(directoryPath, "*.dyn").Any();
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
}

/// <summary>
/// True when the path looks like a Dynamo install's shipped templates folder
/// (…/templates/&lt;locale&gt;). User-chosen custom folders do not match this shape.
/// </summary>
internal static bool IsInstallRootedTemplatesDirectory(string directoryPath)
{
if (string.IsNullOrEmpty(directoryPath))
return false;

try
{
var trimmed = directoryPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var locale = Path.GetFileName(trimmed);
if (!Configurations.SupportedLocaleDic.Values.Contains(locale))
return false;

var templatesFolderName = Path.GetFileName(Path.GetDirectoryName(trimmed));
return string.Equals(templatesFolderName, Configurations.TemplatesAsString, StringComparison.OrdinalIgnoreCase);
}
catch (ArgumentException)
{
return false;
}
}

/// <summary>
/// True when a persisted template path should be replaced with the current install default.
/// Missing/empty folders and previous-install templates\&lt;locale&gt; paths are stale;
/// a user-chosen folder that still contains .dyn files is not.
/// </summary>
internal static bool ShouldResetPersistedTemplatesPath(string persistedPath, string currentDefaultPath)
{
if (string.IsNullOrEmpty(persistedPath))
return true;

if (!IsValidTemplatesDirectory(persistedPath))
return true;

if (!IsInstallRootedTemplatesDirectory(persistedPath))
return false;

return !AreTemplateDirectoriesEqual(persistedPath, currentDefaultPath);
}

private static bool AreTemplateDirectoriesEqual(string first, string second)
{
if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second))
return false;

try
{
return PathHelper.AreDirectoryPathsEqual(first, second);
}
catch (Exception)
{
return string.Equals(first, second, StringComparison.OrdinalIgnoreCase);
}
}

internal bool UpdatePreferenceItemPath(PreferenceItem item, string newLocation)
{
bool isValidFolder = PathHelper.CreateFolderIfNotExist(newLocation) == null;
if (!isValidFolder)
if (string.IsNullOrEmpty(newLocation))
return false;

switch (item)
{
case PreferenceItem.Backup:
if (PathHelper.CreateFolderIfNotExist(newLocation) != null)
return false;
backupDirectory = newLocation;
break;
case PreferenceItem.Templates:
// Do not create this folder. A stale persisted path must stay invalid
// so startup can fall back to the current install's shipped templates.
if (!Directory.Exists(newLocation))
return false;
templatesDirectory = newLocation;
break;
default:
return false;
}
return true;
}
Expand Down
7 changes: 7 additions & 0 deletions src/DynamoCore/Core/DynamoMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ protected virtual DynamoMigratorBase MigrateFrom(DynamoMigratorBase sourceMigrat
// Also exclude SelectedPackagePathForInstall or it may point to previous
// Dynamo version folders. When set to string.empty or null - will default to UserDataFolder.
PreferenceSettings.SelectedPackagePathForInstall = string.Empty;
// TemplateFilePath is install-rooted; reset so the current install's shipped templates are used.
// A user-chosen custom folder that still contains .dyn files is kept.
if (!PathManager.IsValidTemplatesDirectory(PreferenceSettings.TemplateFilePath) ||
PathManager.IsInstallRootedTemplatesDirectory(PreferenceSettings.TemplateFilePath))
{
PreferenceSettings.TemplateFilePath = string.Empty;
}
PreferenceSettings.IronPythonResolveTargetVersion = currentVersionTempPrefs.IronPythonResolveTargetVersion;

return this;
Expand Down
24 changes: 24 additions & 0 deletions src/DynamoCore/Models/DynamoModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,10 @@ private void InitializePreferenceLocations()
{
PreferenceSettings.TemplateFilePath = pathManager.DefaultTemplatesDirectory;
}

// Capture before locale swap so reset logs/saves compare against the value from DynamoSettings.xml.
var persistedTemplatePath = PreferenceSettings.TemplateFilePath;

var supportedLocales = Configurations.SupportedLocaleDic.Values.ToList<string>();

//Get the last part of the template path e.f. if the path is C:\ProgramData\Dynamo\Dynamo Core\templates\en-US then currentPathLocale = en-US
Expand All @@ -1907,6 +1911,14 @@ private void InitializePreferenceLocations()
}
}

if (Core.PathManager.ShouldResetPersistedTemplatesPath(PreferenceSettings.TemplateFilePath, pathManager.DefaultTemplatesDirectory))
{
PreferenceSettings.TemplateFilePath = pathManager.DefaultTemplatesDirectory;
if (!string.Equals(persistedTemplatePath, PreferenceSettings.TemplateFilePath, StringComparison.OrdinalIgnoreCase))
{
Logger.Log($"Configured template path {persistedTemplatePath} is not available; falling back to {PreferenceSettings.TemplateFilePath}");
}
}

UpdatePreferenceItemLocation(PreferenceItem.Backup, PreferenceSettings.BackupLocation);
if (!UpdatePreferenceItemLocation(PreferenceItem.Templates, PreferenceSettings.TemplateFilePath))
Expand All @@ -1917,6 +1929,18 @@ private void InitializePreferenceLocations()
Logger?.Log("Could not use templates location '" + PreferenceSettings.TemplateFilePath +
"'. Falling back to '" + pathManager.TemplatesDirectory + "'.", LogLevel.File);
}
else
{
if (!string.IsNullOrEmpty(PreferenceSettings.TemplateFilePath))
{
PreferenceSettings.AddTrustedLocation(PreferenceSettings.TemplateFilePath);
}

if (!string.Equals(persistedTemplatePath, PreferenceSettings.TemplateFilePath, StringComparison.OrdinalIgnoreCase))
{
PreferenceSettings.SaveInternal(pathManager.PreferenceFilePath);
}
}
}
internal bool UpdatePreferenceItemLocation(PreferenceItem item, string newLocation)
{
Expand Down
89 changes: 88 additions & 1 deletion test/DynamoCoreTests/Configuration/PathManagerTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System.IO;
using Dynamo.Configuration;
using Dynamo.Core;
using NUnit.Framework;

namespace Dynamo.Tests.Configuration
{
[TestFixture]
class PathManagerTests : UnitTestBase
class PathManagerTests : DynamoModelTestBase
{
private static PathManager MakePathManager()
{
Expand All @@ -15,6 +16,79 @@ private static PathManager MakePathManager()
});
}

[Test]
[Category("UnitTests")]
public void WhenUpdatePreferenceItemPathTemplatesAndDirectoryDoesNotExistThenReturnsFalseAndDoesNotCreateDirectory()
{
var pathManager = new PathManager(new PathManagerParams());
var stalePath = Path.Combine(TempFolder, "stale-templates", "en-US");

var updated = pathManager.UpdatePreferenceItemPath(PathManager.PreferenceItem.Templates, stalePath);

Assert.IsFalse(updated);
Assert.IsFalse(Directory.Exists(stalePath));
}

[Test]
[Category("UnitTests")]
public void WhenPersistedTemplatePathDoesNotExistThenTemplatePathResetsToDefault()
{
var missingPath = Path.Combine(TempFolder, "missing-templates");
RestartDynamoWithTemplatePath(missingPath);

var defaultPath = ((PathManager)CurrentDynamoModel.PathManager).DefaultTemplatesDirectory;
Assert.AreEqual(defaultPath, CurrentDynamoModel.PreferenceSettings.TemplateFilePath);
Assert.AreEqual(defaultPath, CurrentDynamoModel.PathManager.TemplatesDirectory);
Assert.IsFalse(Directory.Exists(missingPath));
AssertSavedTemplatePath(defaultPath);
}

[Test]
[Category("UnitTests")]
public void WhenPersistedTemplatePathHasNoGraphsThenTemplatePathResetsToDefault()
{
var emptyPath = Path.Combine(TempFolder, "empty-templates");
Directory.CreateDirectory(emptyPath);
RestartDynamoWithTemplatePath(emptyPath);

var defaultPath = ((PathManager)CurrentDynamoModel.PathManager).DefaultTemplatesDirectory;
Assert.AreEqual(defaultPath, CurrentDynamoModel.PreferenceSettings.TemplateFilePath);
Assert.AreEqual(defaultPath, CurrentDynamoModel.PathManager.TemplatesDirectory);
Assert.IsTrue(Directory.Exists(emptyPath));
AssertSavedTemplatePath(defaultPath);
}

[Test]
[Category("UnitTests")]
public void WhenPersistedTemplatePathIsValidCustomFolderThenTemplatePathIsPreserved()
{
var customPath = Path.Combine(TempFolder, "custom-templates");
Directory.CreateDirectory(customPath);
File.WriteAllText(Path.Combine(customPath, "Custom.dyn"), "{}");
RestartDynamoWithTemplatePath(customPath);

Assert.AreEqual(customPath, CurrentDynamoModel.PreferenceSettings.TemplateFilePath);
Assert.AreEqual(customPath, CurrentDynamoModel.PathManager.TemplatesDirectory);
Assert.IsTrue(CurrentDynamoModel.PreferenceSettings.IsTrustedLocation(customPath));
CurrentDynamoModel.PreferenceSettings.SaveInternal(CurrentDynamoModel.PathManager.PreferenceFilePath);
AssertSavedTemplatePath(customPath);
}

[Test]
[Category("UnitTests")]
public void WhenPersistedTemplatePathIsPreviousInstallThenTemplatePathResetsToDefault()
{
var oldInstallPath = Path.Combine(TempFolder, "old-install", "templates", "en-US");
Directory.CreateDirectory(oldInstallPath);
File.WriteAllText(Path.Combine(oldInstallPath, "Old.dyn"), "{}");
RestartDynamoWithTemplatePath(oldInstallPath);

var defaultPath = ((PathManager)CurrentDynamoModel.PathManager).DefaultTemplatesDirectory;
Assert.AreEqual(defaultPath, CurrentDynamoModel.PreferenceSettings.TemplateFilePath);
Assert.AreNotEqual(oldInstallPath, CurrentDynamoModel.PreferenceSettings.TemplateFilePath);
AssertSavedTemplatePath(defaultPath);
}

/// <summary>
/// DYN-10661: TemplatesDirectory has no default value. It is only ever assigned by
/// UpdatePreferenceItemPath, so a PathManager that has not had that call succeed
Expand Down Expand Up @@ -57,5 +131,18 @@ public void WhenTemplateLocationCannotBeCreatedThenTemplatesDirectoryFallsBackTo
Assert.That(pathManager.TemplatesDirectory, Is.EqualTo(pathManager.DefaultTemplatesDirectory),
"A rejected templates location must leave the default in effect, not null.");
}

private void RestartDynamoWithTemplatePath(string templatePath)
{
CurrentDynamoModel.ShutDown(false);
CurrentDynamoModel = null;
StartDynamo(new PreferenceSettings { TemplateFilePath = templatePath });
}

private void AssertSavedTemplatePath(string expectedPath)
{
var saved = PreferenceSettings.Load(PreferenceSettings.DynamoTestPath);
Assert.AreEqual(expectedPath, saved.TemplateFilePath);
}
}
}
52 changes: 51 additions & 1 deletion test/DynamoCoreTests/UserDataMigrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,15 @@ private IEnumerable<string> MockUserDirectories()
yield return Path.Combine(TempFolder, "2.0");
}

private static void CreateMockPreferenceSettingsFile(string filePath, string packageDir)
private static void CreateMockPreferenceSettingsFile(string filePath, string packageDir, string templateFilePath = null)
{
var settings = new PreferenceSettings
{
CustomPackageFolders = new List<string> { packageDir },
//need to mock this because PreferenceSettings.SelectedPackagePathForInstall uses an event to get UserDataFolder from PathManager
SelectedPackagePathForInstall = packageDir,
IronPythonResolveTargetVersion = new Version(2,4,0).ToString(),
TemplateFilePath = templateFilePath ?? string.Empty,
};
settings.Save(filePath);
}
Expand Down Expand Up @@ -415,5 +416,54 @@ public void IronPythonVersionIsNotMigrated()
// Assert that new ironPythonTargetVersion is not equal to the old version.
Assert.That(targetMigrator.PreferenceSettings.IronPythonResolveTargetVersion, Is.Not.EqualTo(sourcePrefs.IronPythonResolveTargetVersion));
}

[Test]
[Category("UnitTests")]
public void WhenTemplateFilePathIsSetDuringMigrationThenItIsReset()
{
string userDataDir;
CreateMockDirectoriesAndFiles(out userDataDir);

var sourceVersionDir = Path.Combine(userDataDir, "1.3");
var settingsFilePath = Path.Combine(sourceVersionDir, "DynamoSettings.xml");
var staleTemplatePath = Path.Combine(TempFolder, "old-sandbox", "templates", "en-US");

CreateMockPreferenceSettingsFile(settingsFilePath, sourceVersionDir, staleTemplatePath);

var mockPathManager = new Mock<IPathManager>();
var currentVersionDir = Path.Combine(userDataDir, "2.0");
mockPathManager.Setup(x => x.UserDataDirectory).Returns(() => currentVersionDir);

var targetMigrator = DynamoMigratorBase.MigrateBetweenDynamoVersions(
mockPathManager.Object);

var sourcePrefs = PreferenceSettings.Load(settingsFilePath);
Assert.AreEqual(staleTemplatePath, sourcePrefs.TemplateFilePath);
Assert.IsTrue(string.IsNullOrEmpty(targetMigrator.PreferenceSettings.TemplateFilePath));
}

[Test]
[Category("UnitTests")]
public void WhenValidCustomTemplatePathExistsThenMigrationPreservesIt()
{
string userDataDir;
CreateMockDirectoriesAndFiles(out userDataDir);

var customTemplates = Path.Combine(TempFolder, "custom-templates");
Directory.CreateDirectory(customTemplates);
File.WriteAllText(Path.Combine(customTemplates, "MyTemplate.dyn"), "{}");

var sourceVersionDir = Path.Combine(userDataDir, "1.3");
var settingsFilePath = Path.Combine(sourceVersionDir, "DynamoSettings.xml");
CreateMockPreferenceSettingsFile(settingsFilePath, sourceVersionDir, customTemplates);

var mockPathManager = new Mock<IPathManager>();
mockPathManager.Setup(x => x.UserDataDirectory).Returns(() => Path.Combine(userDataDir, "2.0"));

var targetMigrator = DynamoMigratorBase.MigrateBetweenDynamoVersions(
mockPathManager.Object);

Assert.AreEqual(customTemplates, targetMigrator.PreferenceSettings.TemplateFilePath);
}
}
}
Loading