#nullable enable using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using T3.Core.Compilation; using T3.Core.Model; using T3.Core.Operator; using T3.Core.Resource; using T3.Core.Resource.Assets; using T3.Core.Settings; using T3.Editor.Gui.UiHelpers; using T3.Editor.UiModel; namespace T3.Editor.Compilation; /// /// Creates, loads, updates, and unloads editor symbol projects and packages. /// internal static partial class ProjectSetup { public const string EnvironmentVariableName = "T3_ASSEMBLY_PATH"; /// /// Serializes symbol-file saves against package recompiles and reloads. The auto-backup /// saves on a background task while recompilation rewrites the same files and symbol /// dictionaries on the UI thread - both sides must hold this lock. Reentrant. /// internal static readonly object SymbolDataLock = new(); static ProjectSetup() { SetEnvironmentVariable(EnvironmentVariableName, RuntimeAssemblies.CoreDirectory); } public static string ToBasicVersionString(this Version versionPrefix) { return $"{versionPrefix.Major}.{versionPrefix.Minor}.{versionPrefix.Build}"; } private static void SetEnvironmentVariable(string envVar, string envValue) { Environment.SetEnvironmentVariable(envVar, envValue, EnvironmentVariableTarget.Process); // todo - this will not work on linux var existing = Environment.GetEnvironmentVariable(envVar, EnvironmentVariableTarget.User); if (existing == envValue) return; Environment.SetEnvironmentVariable(envVar, envValue, EnvironmentVariableTarget.User); } public static bool TryCreateProject(string nameSpace, bool shareResources, [NotNullWhen(true)] out EditableSymbolProject? newProject, [NotNullWhen(false)] out string? failureLog) { var name = nameSpace.Split('.').Last(); // Filesystem failures (OneDrive virtualisation, broken symlinks, antivirus, // missing/removed drives) surface as exceptions; route them through failureLog. CsProjectFile newCsProj; try { newCsProj = CsProjectFile.CreateNewProject(name, nameSpace, shareResources, UserSettings.Config.ProjectDirectories[0]); } catch (Exception e) { failureLog = $"Failed to create project files on disk: {e.Message}"; Log.Error(failureLog); newProject = null; return false; } if (!newCsProj.TryRecompile(true, out failureLog)) { newProject = null; return false; } newProject = new EditableSymbolProject(newCsProj); if(!newProject.AssemblyInformation.TryGetReleaseInfo(out var releaseInfo)) { failureLog = $"Failed to get release info for project {name}"; newProject.Dispose(); newProject = null; return false; } if (releaseInfo.HomeGuid == Guid.Empty) { failureLog = $"No project home found for project {name}"; newProject = null; return false; } _activePackages.Add(newProject); UpdateSymbolPackage(newProject); InitializePackageResources(newProject); return true; } internal static void RemoveSymbolPackage(EditorSymbolPackage package, bool needsDispose) { if (!needsDispose) { if (!_activePackages.Remove(package)) throw new InvalidOperationException($"Failed to remove package {package}: does not exist"); } else { package.Dispose(); // This will also remove } } private static void AddToLoadedPackages(EditorSymbolPackage package) { if (!_activePackages.Add(package)) throw new InvalidOperationException($"Failed to add package {package.DisplayName} already exists"); } private static void InitializePackageResources(EditorSymbolPackage package) { #if RELEASE if (package.IsReadOnly) return; #endif package.InitializeShaderLinting(ResourcePackageManager.SharedResourcePackages); } public static void DisposePackages() { var allPackages = SymbolPackage.AllPackages.ToArray(); foreach (var package in allPackages) package.Dispose(); } internal static void UpdateSymbolPackage(EditorSymbolPackage package) { UpdateSymbolPackages(package); } public static void UpdateSymbolPackages(params EditorSymbolPackage[] packages) { lock (SymbolDataLock) { UpdateSymbolPackagesInternal(packages); } } private static void UpdateSymbolPackagesInternal(EditorSymbolPackage[] packages) { var parallel = UserSettings.Config.LoadMultiThreaded; var stopWatch = Stopwatch.StartNew(); // Actually update the symbol packages // this switch statement exists to avoid the overhead of parallelization for a single package, e.g. when compiling changes to a single project switch (packages.Length) { case 0: Log.Warning($"Tried to update symbol packages but none were provided"); return; case 1: { Log.Debug("Updating symbol packages " + (parallel ? "(parallel)":"")); var package = packages[0]; // ADD THIS: Ensure context is generated and types are scanned // before symbols are loaded and children are instantiated. package.AssemblyInformation.GenerateLoadContext(); package.LoadSymbols(parallel, out var newlyRead, out var allNewSymbols); SymbolPackage.ApplySymbolChildren(newlyRead); package.LoadUiFiles(parallel, allNewSymbols, out var newlyLoadedUis, out var preExistingUis); package.LocateSourceCodeFiles(); package.RegisterUiSymbols(newlyLoadedUis, preExistingUis); var count = package.Symbols.Sum(x => x.Value.InstancesOfSelf.Count()); Log.Debug($"Updated symbol package {package.DisplayName} in {stopWatch.ElapsedMilliseconds}ms with {count} instances of its symbols"); // Check for packages that were cascadingly unloaded due to cross-context // assembly dependencies (e.g. Mediapipe unloads when Lib's context unloads // because Mediapipe resolved shared assemblies from Lib's context). var cascaded = _activePackages.Where(x => x.NeedsAssemblyLoad).ToArray(); if (cascaded.Length > 0) { Log.Info($"Reloading {cascaded.Length} packages that were cascadingly unloaded..."); UpdateSymbolPackages(cascaded); } return; } } // do the same as above, just in several steps so we can do them in parallel ConcurrentDictionary> loadedSymbols = new(); ConcurrentDictionary> loadedOrCreatedSymbols = new(); // generate load contexts synchronously foreach (var package in packages) { package.AssemblyInformation.GenerateLoadContext(); } Log.Info("Loading symbols..."); if (parallel) { packages .AsParallel() .ForAll(package => //pull out for non-editable ones too { package.LoadSymbols(parallel, out var newlyRead, out var allNewSymbols); loadedSymbols.TryAdd(package, newlyRead); loadedOrCreatedSymbols.TryAdd(package, allNewSymbols); }); } else { for (var index = packages.Length - 1; index >= 0; index--) { var package = packages[index]; package.LoadSymbols(parallel, out var newlyRead, out var allNewSymbols); loadedSymbols.TryAdd(package, newlyRead); loadedOrCreatedSymbols.TryAdd(package, allNewSymbols); } } Log.Info("Applying children..."); loadedSymbols .AsParallel() .ForAll(pair => SymbolPackage.ApplySymbolChildren(pair.Value)); Log.Info("Loading symbol UIs..."); ConcurrentDictionary loadedSymbolUis = new(); packages .AsParallel() .ForAll(package => { var newlyRead = loadedOrCreatedSymbols[package]; package.LoadUiFiles(false, newlyRead, out var newlyReadUis, out var preExisting); loadedSymbolUis.TryAdd(package, new SymbolUiLoadInfo(newlyReadUis, preExisting)); }); Log.Info("Locating Source code files..."); loadedSymbolUis .AsParallel() .ForAll(pair => { pair.Key.LocateSourceCodeFiles(); }); foreach (var (symbolPackage, symbolUis) in loadedSymbolUis) { symbolPackage.RegisterUiSymbols(symbolUis.NewlyLoaded, symbolUis.PreExisting); } Log.Debug($">> Updated {packages.Length} symbol packages in {stopWatch.ElapsedMilliseconds/1000:0.0}s"); var needingReload = _activePackages.Where(x => x.NeedsAssemblyLoad).ToArray(); if (needingReload.Length > 0) { Log.Info($"Reloading {needingReload.Length} packages that need reloading..."); UpdateSymbolPackages(needingReload); } } public static void SetProjectArchived(CsProjectFile projectFile, bool archive) { projectFile.IsArchived = archive; if (archive) { // 1. Find and Unload the active package if it exists var package = _activePackages.FirstOrDefault(p => p is EditableSymbolProject ep && ep.CsProjectFile == projectFile); if (package != null) { RemoveSymbolPackage(package, needsDispose: true); // Stops watchers and cleans up } if (ArchivedProjects.All(p => p.ProjectFile != projectFile)) { ArchivedProjects.Add(new ArchivedProjectInfo(projectFile)); } } else { // 1. Remove from archived list ArchivedProjects.RemoveAll(p => p.ProjectFile == projectFile); // 2. Initialize the project var newProject = new EditableSymbolProject(projectFile); // 3. Register Assets (Missing from your current implementation) // This mirrors the 'Initial Startup Migration' phase in LoadAll AssetRegistry.RegisterAssetsFromPackage(newProject); // 4. Register the package so it's visible globally AddToLoadedPackages(newProject); // 5. Critical: Generate the assembly context for type resolution newProject.AssemblyInformation.GenerateLoadContext(); // 6. Load symbols and UIs UpdateSymbolPackages(newProject); // 7. Initialize shader linting and package resources InitializePackageResources(newProject); // 8. Force re-evaluation of instances // This helps fix the "Failed to create child instances" by poking the registry foreach (var symbol in newProject.Symbols.Values) { SymbolPackage.UpdateSymbolInstances(symbol); } } EditorSymbolPackage.NotifySymbolStructureChange(); } internal sealed record ArchivedProjectInfo(CsProjectFile ProjectFile) : IResourcePackage { public string DisplayName => ProjectFile.Name; public string Name => ProjectFile.Name; public Guid Id => ProjectFile.PackageId; public string Folder => ProjectFile.Directory; public string AssetsFolder => Path.Combine(Folder, FileLocations.AssetsSubfolder); public string? RootNamespace => ProjectFile.RootNamespace; // Archived projects don't actively watch files or track live dependencies public ResourceFileWatcher? FileWatcher => null; public bool IsReadOnly => true; public IReadOnlyCollection Dependencies => Array.Empty(); } /// /// A project that failed to load at startup (could not be read or compiled). Kept so the Hub can /// surface it and offer recovery instead of the project silently disappearing. Not persisted — /// rebuilt on each startup from the load results. is null when the /// .csproj itself could not be parsed. /// internal sealed record BrokenProjectInfo(FileInfo FileInfo, CsProjectFile? ProjectFile, string Reason, string? Hint) { public string Name => ProjectFile?.Name ?? Path.GetFileNameWithoutExtension(FileInfo.FullName); public string Folder => ProjectFile?.Directory ?? FileInfo.DirectoryName ?? string.Empty; public string? RootNamespace => ProjectFile?.RootNamespace; } private static readonly HashSet _activePackages = []; internal static readonly List ArchivedProjects = []; internal static readonly List BrokenProjects = []; internal static readonly IEnumerable AllPackages = _activePackages; private readonly record struct SymbolUiLoadInfo(SymbolUi[] NewlyLoaded, SymbolUi[] PreExisting); }