Files
2026-07-13 13:13:17 +08:00

549 lines
19 KiB
C#

#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using T3.Core.Compilation;
using T3.Core.IO;
using T3.Core.Logging;
using T3.Core.Operator;
using T3.Core.Resource;
using T3.Core.Resource.Assets;
using T3.Core.Stats;
using T3.Core.Settings;
// ReSharper disable RedundantNameQualifier
namespace T3.Core.Model;
/// <summary>
/// The runtime-loaded Symbol information and base class of essentially what is a read only project.
/// </summary>
/// <remarks>
/// Regarding naming, we consider all t3 operator packages as packages for the sake of consistency with future nuget terminology etc.
/// -- only the user's editable "packages" are referred to as projects
///
/// Is used to create an EditorSymbolPackage.
///</remarks>
public abstract partial class SymbolPackage : IResourcePackage
{
public virtual ResourceFileWatcher? FileWatcher => null;
public bool IsSharingResources => AssemblyInformation.ShouldShareResources;
public readonly bool DoNotIncludedSharedPackages;
public virtual bool IsReadOnly => true;
public readonly AssemblyInformation AssemblyInformation;
/** Primary directory of the package that contains the csproj, home and other other data */
public string Folder { get; }
public virtual string DisplayName => AssemblyInformation.Name;
/** For readonly packages we search for .t3 files in the Symbols folder */
protected virtual IEnumerable<string> SymbolSearchFiles
{
get
{
var dir = Path.Combine(Folder, FileLocations.ReleaseSymbolsSubfolder);
if (!Directory.Exists(dir))
return [];
return Directory.EnumerateFiles(dir, $"*{SymbolExtension}", SearchOption.AllDirectories);
}
}
protected event Action<string?, Symbol>? SymbolAdded;
protected event Action<Symbol>? SymbolUpdated;
protected event Action<Guid>? SymbolRemoved;
private static ConcurrentBag<SymbolPackage> _allPackages = [];
public static IEnumerable<SymbolPackage> AllPackages => _allPackages;
public string AssetsFolder { get; private set; } = null!;
public IReadOnlyCollection<DependencyCounter> Dependencies => (ReadOnlyCollection<DependencyCounter>)DependencyDict.Values;
protected readonly ConcurrentDictionary<SymbolPackage, DependencyCounter> DependencyDict = new();
public string RootNamespace => ReleaseInfo.RootNamespace;
protected virtual ReleaseInfo ReleaseInfo
{
get
{
if (AssemblyInformation.TryGetReleaseInfo(out var releaseInfo))
return releaseInfo;
throw new InvalidOperationException($"Failed to get release info for package {AssemblyInformation.Name}");
}
}
public string Name
{
get
{
// 1. If AssemblyInfo already has a name (Debug/Dev), use it.
if (!string.IsNullOrWhiteSpace(AssemblyInformation.Name))
return AssemblyInformation.Name;
// 2. Fallback to ReleaseInfo's RootNamespace (Release build).
// Accessing ReleaseInfo triggers TryGetReleaseInfo() internally.
try
{
return ReleaseInfo.RootNamespace;
}
catch (Exception)
{
// 3. Last resort fallback
return AssemblyInformation.Name ?? string.Empty;
}
}
}
public Guid Id => AssemblyInformation.Id != Guid.Empty
? AssemblyInformation.Id
: ReleaseInfo.PackageId; // Use a dedicated PackageId field
static SymbolPackage()
{
// OpUpdateCounter registers itself in its constructor.
_ = new OpUpdateCounter();
RegisterTypes();
}
protected SymbolPackage(AssemblyInformation assembly, string? mainDirectory, bool initializeResources)
{
AssemblyInformation = assembly;
Folder = mainDirectory ?? assembly.Directory;
lock (_allPackages)
_allPackages.Add(this);
DoNotIncludedSharedPackages = assembly.Name == FileLocations.LibPackageName;
// This can be delayed for EditableSymbolPackages and the called directly from there
if (initializeResources)
{
// ReSharper disable once VirtualMemberCallInConstructor
InitializeAssets();
}
}
protected virtual void InitializeAssets()
{
AssetsFolder = Path.Combine(Folder, FileLocations.AssetsSubfolder);
// Force the assembly information to load the JSON metadata now
// This ensures Name and Id are valid before registration starts.
_ = ReleaseInfo;
// Avoid creating resource folder in protected program folder
if (!IsReadOnly)
{
Directory.CreateDirectory(AssetsFolder);
}
ResourcePackageManager.AddSharedResourceFolder(this, AssemblyInformation.ShouldShareResources);
AssetRegistry.RegisterAssetsFromPackage(this);
}
public virtual void Dispose()
{
AssetRegistry.UnregisterPackage(Id);
ResourcePackageManager.RemoveSharedResourceFolder(this);
ClearSymbols();
var currentPackages = _allPackages.ToList();
currentPackages.Remove(this);
lock (_allPackages)
_allPackages = new ConcurrentBag<SymbolPackage>(currentPackages);
AssemblyInformation.Unload();
// Todo - symbol instance destruction...?
return;
void ClearSymbols()
{
if (SymbolDict.Count == 0)
return;
var symbols = SymbolDict.Values.ToArray();
foreach (var symbol in symbols)
{
var id = symbol.Id;
SymbolDict.Remove(id, out _);
}
}
}
/// <summary>
/// Loads symbols from the assembly and locates their symbol .t3/json files
/// </summary>
/// <param name="parallel">if true, parallel loading is used</param>
/// <param name="newlyRead">Newly read json files for new types</param>
/// <param name="allNewSymbols">All new symbols, including those for which a json file was not found</param>
public void LoadSymbols(bool parallel, out List<SymbolJson.SymbolReadResult> newlyRead, out List<Symbol> allNewSymbols)
{
Log.Debug($" Loading {AssemblyInformation.Name}...");
if (!AssemblyInformation.TryLoadTypes())
{
var error = $"Failed to load types for {AssemblyInformation.Name}";
Log.Error(error);
newlyRead = [];
allNewSymbols = [];
return;
}
IDictionary<Guid, Type> newTypes;
// Computed by set difference up front: LoadTypes may run in parallel, and removing from a
// shared HashSet inside it raced — lost removals made live symbols appear deleted.
var removedSymbolIds = new HashSet<Guid>();
foreach (var id in SymbolDict.Keys)
{
if (!AssemblyInformation.OperatorTypeInfo.ContainsKey(id))
removedSymbolIds.Add(id);
}
ConcurrentBag<Symbol> updatedSymbols = new();
if (parallel)
{
newTypes = new ConcurrentDictionary<Guid, Type>();
AssemblyInformation.OperatorTypeInfo
.AsParallel()
.ForAll(kvp => LoadTypes(kvp.Key, kvp.Value.Type, newTypes, updatedSymbols));
}
else
{
newTypes = new Dictionary<Guid, Type>();
foreach (var (guid, type) in AssemblyInformation.OperatorTypeInfo)
{
LoadTypes(guid, type.Type, newTypes, updatedSymbols);
}
}
// gather slot changes
foreach (var symbol in updatedSymbols)
{
symbol.UpdateInstanceType(true);
}
// update symbol instances
foreach (var symbol in updatedSymbols)
{
UpdateSymbolInstances(symbol);
SymbolUpdated?.Invoke(symbol);
}
if (SymbolRemoved != null)
{
// remaining symbols have been removed from the assembly
foreach (var symbolId in removedSymbolIds)
{
SymbolRemoved.Invoke(symbolId);
}
}
newlyRead = [];
allNewSymbols = [];
if (newTypes.Count != 0)
{
_corruptedSymbolFiles.Clear();
var searchFileEnumerator = parallel ? SymbolSearchFiles.AsParallel() : SymbolSearchFiles;
var symbolsRead = searchFileEnumerator
.Select(TryReadSymbolFile)
.Where(result => result != null)
.Select(result =>
{
var file = result!;
if (!newTypes.TryGetValue(file.Guid, out var type))
return default;
return ReadSymbolFromJsonFileResult(file, type);
})
.Where(symbolReadResult => symbolReadResult.Result.Symbol is not null)
.ToArray();
if (CoreSettings.Config.LogCompilationDetails)
Log.Debug($"{AssemblyInformation.Name}: Registering loaded symbols...");
foreach (var readSymbolResult in symbolsRead)
{
var symbol = readSymbolResult.Result.Symbol;
var id = symbol!.Id;
if (!SymbolDict.TryAdd(id, symbol))
{
Log.Error($"Can't load symbol for [{symbol.Name}]. Registry already contains id {symbol.Id}: [{SymbolDict[symbol.Id].Name}]");
continue;
}
newlyRead.Add(readSymbolResult.Result);
newTypes.Remove(id, out _);
allNewSymbols.Add(symbol);
SymbolAdded?.Invoke(readSymbolResult.Path, symbol);
}
}
// these do not have a symbol json file but are defined in the assembly
foreach (var (guid, newType) in newTypes)
{
var symbol = CreateSymbol(newType, guid);
var id = symbol.Id;
if (!SymbolDict.TryAdd(id, symbol))
{
Log.Error($"{AssemblyInformation.Name}: Ignoring redefinition symbol {symbol.Name}.");
continue;
}
//Log.Debug($"{AssemblyInformation.Name}: new added symbol: {newType}");
allNewSymbols.Add(symbol);
SymbolAdded?.Invoke(null, symbol);
}
OnSymbolsLoaded();
return;
void LoadTypes(Guid guid, Type type, IDictionary<Guid, Type> newTypesDict, ConcurrentBag<Symbol> updated)
{
if (SymbolDict.TryGetValue(guid, out var symbol))
{
if (symbol == null) // this should never happen??
{
Log.Error($"Skipping update of invalid symbol {guid}. Symbol entry was null - this is a bug.");
return;
}
// we already have this symbol, so mark it as updated (but dont actually update it yet)
symbol.UpdateTypeWithoutUpdatingDefinitionsOrInstances(type, this);
updated.Add(symbol);
}
else
{
// it's a new type!!
if (!newTypesDict.TryAdd(guid, type))
{
Log.Error($"{DisplayName}: Failed to add new type {type} with guid '{guid}' - " +
"is there another operator type with the same guid in this project?");
}
}
}
SymbolJsonResult ReadSymbolFromJsonFileResult(JsonFileResult<Symbol> jsonInfo, Type type)
{
try
{
var result = SymbolJson.ReadSymbolRoot(jsonInfo.Guid, jsonInfo.JToken, type, this);
jsonInfo.Object = result.Symbol;
return new SymbolJsonResult(result, jsonInfo.FilePath);
}
catch (Exception e)
{
// Skip a structurally-broken symbol instead of aborting the whole load; recorded so the
// package won't be saved over. The default result is dropped by the caller's Where.
Log.Error($"Skipping corrupted symbol file '{jsonInfo.FilePath}': {e.Message}");
_corruptedSymbolFiles.Add(jsonInfo.FilePath);
return default;
}
}
JsonFileResult<Symbol>? TryReadSymbolFile(string filePath)
{
try
{
return JsonFileResult<Symbol>.ReadAndCreate(filePath);
}
catch (Exception e)
{
// A single corrupt/unreadable .t3 must not abort loading every project. Skip it — the
// symbol goes missing (parents referencing it are protected by
// Symbol.HasUnresolvedChildren) and the file is recorded for restore-from-backup.
Log.Error($"Skipping corrupted symbol file '{filePath}': {e.Message}");
_corruptedSymbolFiles.Add(filePath);
return null;
}
}
}
protected virtual void OnSymbolsLoaded()
{
}
public static void UpdateSymbolInstances(Symbol symbol, bool forceTypeUpdate = false)
{
symbol.UpdateInstanceType(forceTypeUpdate);
symbol.CreateAnimationUpdateActionsForSymbolInstances();
}
protected internal Symbol CreateSymbol(Type instanceType, Guid id)
{
return new Symbol(instanceType, id, this);
}
public static void ApplySymbolChildren(List<SymbolJson.SymbolReadResult> symbolsRead)
{
Parallel.ForEach(symbolsRead, result => TryReadAndApplyChildren(result));
}
protected static bool TryReadAndApplyChildren(SymbolJson.SymbolReadResult result)
{
if (SymbolJson.TryReadAndApplySymbolChildren(result)) return true;
var symbol = result.Symbol;
if (symbol == null)
{
Log.Error($"Problem obtaining children of 'null' with {result.ChildrenJsonArray.Length} children");
return false;
}
Log.Error($"Problem obtaining children of {symbol.Name ?? "'null'"} ({symbol.Id})");
return false;
}
public readonly record struct SymbolJsonResult(in SymbolJson.SymbolReadResult Result, string Path);
public IReadOnlyDictionary<Guid, Symbol> Symbols => SymbolDict;
private readonly ConcurrentBag<string> _corruptedSymbolFiles = new();
/// <summary>
/// Paths of .t3 files that were corrupt/unreadable during the last load and were skipped. When
/// non-empty the editor refuses to save this package, so a symbol re-created empty from its type
/// can't overwrite a file a backup could still recover.
/// </summary>
public IReadOnlyCollection<string> CorruptedSymbolFilePaths => _corruptedSymbolFiles;
/// <summary>Cheap per-frame check (unlike <see cref="CorruptedSymbolFilePaths"/>.Count) for UI flags.</summary>
public bool HasCorruptedSymbolFiles => !_corruptedSymbolFiles.IsEmpty;
/// <summary>Records a corrupt/unreadable symbol or symbol-UI file so the package won't be saved over it.</summary>
protected void RecordCorruptedSymbolFile(string filePath) => _corruptedSymbolFiles.Add(filePath);
protected readonly ConcurrentDictionary<Guid, Symbol> SymbolDict = new();
public const string SymbolExtension = ".t3";
public bool ContainsSymbolName(ReadOnlySpan<char> newSymbolName, ReadOnlySpan<char> symbolNamespace)
{
foreach (var existing in SymbolDict.Values)
{
var existingNamespace = existing.Namespace.AsSpan();
var existingName = existing.Name.AsSpan();
if (newSymbolName.SequenceEqual(existingName) && symbolNamespace.SequenceEqual(existingNamespace))
return true;
}
return false;
}
public void AddResourceDependencyOn(FileResource resource)
{
if (!TryGetDependencyCounter(resource, out var dependencyCount))
return;
dependencyCount.ResourceCount++;
}
public void RemoveResourceDependencyOn(FileResource fileResource)
{
if (!TryGetDependencyCounter(fileResource, out var dependency))
return;
dependency.ResourceCount--;
RemoveIfNoRemainingReferences(dependency);
}
public void AddDependencyOn(Symbol symbol)
{
if (symbol.SymbolPackage == this)
return;
if (!TryGetDependencyCounter(symbol, out var dependency))
return;
if (dependency.SymbolChildCount++ == 0)
{
// this is the first reference to this package
DependencyDict.TryAdd((SymbolPackage)dependency.Package, dependency);
}
}
public void RemoveDependencyOn(Symbol symbol)
{
if (symbol.SymbolPackage == this)
return;
if (!TryGetDependencyCounter(symbol, out var dependency))
return;
dependency.SymbolChildCount--;
RemoveIfNoRemainingReferences(dependency);
}
private void RemoveIfNoRemainingReferences(DependencyCounter dependency)
{
if (dependency is { SymbolChildCount: 0, ResourceCount: 0 })
{
DependencyDict.Remove((SymbolPackage)dependency.Package, out _);
}
}
private bool TryGetDependencyCounter(IResource fileResource, [NotNullWhen(true)] out DependencyCounter? dependencyCounter)
{
var owningPackage = fileResource.OwningPackage;
if (owningPackage is not SymbolPackage symbolPackage)
{
dependencyCounter = null;
return false;
}
if (DependencyDict.TryGetValue(symbolPackage, out dependencyCounter))
return true;
dependencyCounter = new DependencyCounter
{
Package = symbolPackage
};
if (DependencyDict.TryAdd(symbolPackage, dependencyCounter))
{
// this is the first reference to this package
return true;
}
// another thread added it in the meantime??
return DependencyDict.TryGetValue(symbolPackage, out dependencyCounter);
}
public bool OwnsNamespace(string namespaceName)
{
return namespaceName == RootNamespace
|| namespaceName.StartsWith(RootNamespace)
|| AssemblyInformation.Namespaces.Contains(namespaceName)
|| AssemblyInformation.Namespaces.Any(x => namespaceName.StartsWith(x));
}
}
public sealed record DependencyCounter
{
public required IResourcePackage Package { get; init; }
internal int SymbolChildCount { get; set; }
internal int ResourceCount { get; set; }
public override string ToString()
{
return $"{Package.DisplayName}: Symbol References: {SymbolChildCount}, Resource References: {ResourceCount}";
}
}