#nullable enable using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using T3.Core.Compilation; using T3.Core.IO; using T3.Core.Model; using T3.Core.Operator; using T3.Core.Resource; using T3.Core.Settings; using T3.Core.Utils; using T3.Editor.External; namespace T3.Editor.UiModel; // todo - make abstract, create NugetSymbolPackage /// /// EditorSymbolPackage is a readonly with s. /// /// /// Is used to create an . /// internal class EditorSymbolPackage : SymbolPackage { /// /// Constructor for a successfully compiled package /// /// Main assembly of the package /// The main package directory, if different from the directory from the provided assembly information /// public EditorSymbolPackage(AssemblyInformation assembly, string? directory, bool initializeResources = true) : base(assembly, directory, initializeResources) { if(CoreSettings.Config.LogCompilationDetails) Log.Debug($"Added package {assembly.Name}"); SymbolAdded += OnSymbolAdded; assembly.Unloaded += OnAssemblyUnloaded; assembly.UnloadComplete += OnAssemblyUnloadComplete; } private void OnAssemblyUnloadComplete(AssemblyInformation obj) { UnloadInProgress = false; } protected virtual void OnSymbolAdded(string? path, Symbol symbol) { var id = symbol.Id; if (_filePathHandlers.TryGetValue(id, out var handler)) { // A symbol removed and re-added across recompiles gets a new Symbol object under the same id - // re-point the surviving handler so it doesn't act on the stale instance. handler.Symbol = symbol; return; } handler = new SymbolPathHandler(symbol, path); _filePathHandlers[id] = handler; } protected virtual void OnSymbolUiLoaded(string? path, SymbolUi symbolUi) { var id = symbolUi.Symbol.Id; _filePathHandlers[id].UiFilePath = path; } private void OnSourceCodeLocated(string path, Guid guid) { if (_filePathHandlers.TryGetValue(guid, out var handler)) { handler.SourceCodePath = path; } else { Log.Error($"No file path handler found for {guid} / '{path}'"); } } /// /// Loads UI files for the provided symbols. /// This method does not delete UIs that have since been deleted /// /// If true, load and process in parallel /// The symbols for which UI files should be loaded - just the new ones /// Brand spankin new symbol UIs /// Symbol UIs that already existed at runtime - if this is the first time this package was loaded, this will be empty public void LoadUiFiles(bool parallel, List newlyReadSymbols, out SymbolUi[] newlyReadSymbolUis, out SymbolUi[] preExistingSymbolUis) { NeedsAssemblyLoad = false; var newSymbols = newlyReadSymbols.ToDictionary(result => result.Id, symbol => symbol); var newSymbolsWithoutUis = new ConcurrentDictionary(newSymbols); preExistingSymbolUis = SymbolUiDict.Values.ToArray(); if(CoreSettings.Config.LogCompilationDetails) Log.Debug($"{AssemblyInformation.Name}: Loading Symbol UIs from \"{Folder}\""); var enumerator = parallel ? SymbolUiSearchFiles.AsParallel() : SymbolUiSearchFiles; var newlyReadSymbolUiList = enumerator .Select(TryReadSymbolUiFile) .Where(result => result != null && newSymbols.ContainsKey(result.Guid)) .Select(uiJson => { var file = uiJson!; if (!SymbolUiJson.TryReadSymbolUi(file.JToken, newSymbols[file.Guid], out var symbolUi)) { Log.Error($"Error reading symbol Ui for {file.Guid} from file \"{file.FilePath}\""); return null; } newSymbolsWithoutUis.Remove(symbolUi.Symbol.Id, out _); var id = symbolUi.Symbol.Id; if (!SymbolUiDict.TryAdd(id, symbolUi)) { Log.Error($"{AssemblyInformation.Name}: Duplicate symbol UI for {symbolUi.Symbol.Name}?"); return null; } OnSymbolUiLoaded(file.FilePath, symbolUi); return symbolUi; }) .Where(symbolUi => symbolUi != null) .Select(symbolUi => symbolUi!) .ToList(); if (newSymbolsWithoutUis.Count == 0) { newlyReadSymbolUis = newlyReadSymbolUiList.ToArray(); return; } foreach (var (guid, symbol) in newSymbolsWithoutUis) { var symbolUi = new SymbolUi(symbol, false); if (!SymbolUiDict.TryAdd(guid, symbolUi)) { Log.Error($"{AssemblyInformation.Name}: Duplicate symbol UI for {symbol.Name}?"); continue; } newlyReadSymbolUiList.Add(symbolUi); OnSymbolUiLoaded(null, symbolUi); } newlyReadSymbolUis = newlyReadSymbolUiList.ToArray(); return; JsonFileResult? TryReadSymbolUiFile(string filePath) { try { return JsonFileResult.ReadAndCreate(filePath); } catch (Exception e) { // A corrupt .t3ui holds only UI layout, not the graph — but it must not abort the load. // Skip it (the symbol falls back to default UI) and record it so the package won't be // saved over, keeping the file recoverable from a backup. Log.Error($"Skipping corrupted symbol UI file '{filePath}': {e.Message}"); RecordCorruptedSymbolFile(filePath); return null; } } } public void RegisterUiSymbols(SymbolUi[] newSymbolUis, SymbolUi[] preExistingSymbolUis) { if(CoreSettings.Config.LogCompilationDetails) Log.Debug($@"{DisplayName}: Registering UI entries..."); foreach (var symbolUi in preExistingSymbolUis) { RegisterSymbolUi(symbolUi); } foreach (var symbolUi in newSymbolUis) { RegisterSymbolUi(symbolUi); } return; void RegisterSymbolUi(SymbolUi symbolUi) { symbolUi.UpdateConsistencyWithSymbol(); symbolUi.ClearModifiedFlag(); //Log.Debug($"Add UI for {symbolUi.Symbol.Name} {symbolUi.Symbol.Id}"); } } public override void Dispose() { ClearSymbolUis(); base.Dispose(); ShaderLinter.RemovePackage(this); return; void ClearSymbolUis() { var symbolUis = SymbolUiDict.Values.ToArray(); foreach (var symbolUi in symbolUis) { try { var symbol = symbolUi.Symbol; SymbolUiDict.TryRemove(symbol.Id, out _); } catch (KeyNotFoundException) { Log.Warning("Can't remove obsolete symbol."); } } } } /// /// Looks for source codes in the project folder and subfolders and tries to find the symbol id in the source code /// public virtual void LocateSourceCodeFiles() { #if DEBUG int sourceCodeCount = 0; int sourceCodeAttempts = 0; #endif SourceCodeSearchFiles .AsParallel() .ForAll(ParseCodeFile); #if DEBUG if (sourceCodeCount == 0 && sourceCodeAttempts != 0) { Log.Error($"{AssemblyInformation.Name}: No source code files found in project folder."); } else { if(CoreSettings.Config.LogCompilationDetails) Log.Debug($"{AssemblyInformation.Name}: Found {sourceCodeCount} operator source code files out of {sourceCodeAttempts} C# files."); } #endif return; void ParseCodeFile(string file) { #if DEBUG Interlocked.Increment(ref sourceCodeAttempts); #endif var streamReader = new StreamReader(file); var guid = Guid.Empty; while (!streamReader.EndOfStream) { var line = streamReader.ReadLine(); if (line == null) break; if (!StringUtils.TryFindIgnoringAllWhitespace(line, "[Guid(\"", StringUtils.SearchResultIndex.AfterTerm, out var guidStartIndex)) continue; var indexOfQuote = line.IndexOf('"', guidStartIndex); var guidSpan = line.AsSpan(guidStartIndex, indexOfQuote - guidStartIndex); if (!Guid.TryParse(guidSpan, out guid)) { Log.Error($"{DisplayName}: Failed to parse guid from {guidSpan.ToString()} in \"{file}\""); continue; } break; } streamReader.Close(); streamReader.Dispose(); if (guid == Guid.Empty) return; #if DEBUG Interlocked.Increment(ref sourceCodeCount); #endif OnSourceCodeLocated(file, guid); } } public bool TryGetSourceCodePath(Symbol symbol, out string? path) { if (_filePathHandlers.TryGetValue(symbol.Id, out var filePathInfo)) { path = filePathInfo.SourceCodePath; return path != null; } path = null; return false; } public void InitializeShaderLinting(IReadOnlyList sharedShaderPackages) { ShaderLinter.AddPackage(this, sharedShaderPackages); } internal bool HasHome { get { if (HasHomeSymbol(out var error)) return true; if (error != null) Log.Warning(error); return false; } } public bool HasHomeSymbol(out string? error) { error = null; try { if (HomeSymbolId == Guid.Empty) return false; if (Symbols.ContainsKey(HomeSymbolId)) return true; if (Symbols.Count == 0) { error = $"Package {Name} has no Symbols definition."; return false; } error = $"Home symbol {HomeSymbolId} not found"; return false; } catch (Exception e) { error = "Failed to parse project: " + e.Message; return false; } } protected readonly ConcurrentDictionary SymbolUiDict = new(); public IReadOnlyDictionary SymbolUis => SymbolUiDict; protected virtual IEnumerable SymbolUiSearchFiles { get { var dir = Path.Combine(Folder, FileLocations.SymbolUiSubFolder); if (!Directory.Exists(dir)) { return []; } return Directory.EnumerateFiles(dir, $"*{SymbolUiExtension}", SearchOption.AllDirectories); } } protected virtual IEnumerable SourceCodeSearchFiles { get { var dir = Path.Combine(Folder, FileLocations.SourceCodeSubFolder); if (!Directory.Exists(dir)) { return []; } return Directory.EnumerateFiles(dir, $"*{SourceCodeExtension}", SearchOption.AllDirectories); } } private readonly ConcurrentDictionary _filePathHandlers = new(); protected IDictionary FilePathHandlers => _filePathHandlers; public Guid HomeSymbolId => OverrideHomeGuid != Guid.Empty ? OverrideHomeGuid : ReleaseInfo.HomeGuid; public Guid OverrideHomeGuid = Guid.Empty; internal const string SourceCodeExtension = ".cs"; public const string SymbolUiExtension = ".t3ui"; public static IEnumerable AllSymbols => AllPackages .Cast() .Select(x => x.SymbolDict) .SelectMany(x => x.Values); public static IEnumerable AllSymbolUis => AllPackages .Cast() .Select(x => x.SymbolUiDict) .SelectMany(x => x.Values); public void Reload(SymbolUi symbolUi) { var symbol = symbolUi.Symbol; var id = symbol.Id; if (!_filePathHandlers.TryGetValue(id, out var pathHandler)) { throw new Exception($"No path handler found for symbol {id}"); } var symbolPath = pathHandler.SymbolFilePath; if (symbolPath == null) { throw new Exception($"No symbol path found for symbol {id}"); } var symbolUiPath = pathHandler.UiFilePath; if (symbolUiPath == null) { throw new Exception($"No symbol ui path found for symbol {id}"); } // reload single ui JsonFileResult symbolJson; try { symbolJson = JsonFileResult.ReadAndCreate(symbolPath); } catch (Exception e) { Log.Error($"Not reloading [{symbol}]: its symbol file is corrupt ({e.Message}). Keeping the loaded version."); return; } var result = SymbolJson.ReadSymbolRoot(symbol.Id, symbolJson.JToken, symbol.InstanceType, this); if (result.Symbol == null) { Log.Error($"Failed to reload read-only symbol {symbol} for {id} from {symbolPath}"); return; } symbol.ReplaceWithContentsOf(result.Symbol); // hacky workaround to avoid creating duplicate children in the next step var fakeResult = result with { Symbol = symbol }; if (!TryReadAndApplyChildren(fakeResult)) { Log.Error($"Failed to reload symbol for symbol {id}"); return; } UpdateSymbolInstances(symbol, forceTypeUpdate: true); JsonFileResult symbolUiJson; try { symbolUiJson = JsonFileResult.ReadAndCreate(symbolUiPath); } catch (Exception e) { Log.Error($"Not reloading UI for [{symbol}]: its .t3ui file is corrupt ({e.Message}). Keeping the loaded version."); return; } if (!SymbolUiJson.TryReadSymbolUi(symbolUiJson.JToken, symbol, out var newSymbolUi)) { throw new Exception($"Failed to reload symbol ui for symbol {id}"); } // override registry values newSymbolUi.UpdateConsistencyWithSymbol(); symbolUi.ReplaceWith(newSymbolUi); symbolUi.ClearModifiedFlag(); } public bool TryGetSymbolUi(Guid rSymbolId, [NotNullWhen(true)] out SymbolUi? symbolUi) { return SymbolUiDict.TryGetValue(rSymbolId, out symbolUi); } // todo - output should be an IDisposable wrapper and RemoveSymbolUi should be called in Dispose and made private internal bool TryCreateNewSymbol([NotNullWhen(true)] out SymbolUi? symbolUi) { var containerOp = CreateSymbol(typeof(T), Guid.NewGuid()); if (!SymbolDict.TryAdd(containerOp.Id, containerOp)) { Log.Error($"Failed to add new symbol for {containerOp.Name} ({containerOp.Id})"); symbolUi = null; return false; } symbolUi = new SymbolUi(containerOp, true); if (SymbolUiDict.TryAdd(containerOp.Id, symbolUi)) return true; Log.Error($"Failed to add new symbol ui for {containerOp.Name} ({containerOp.Id})"); return false; } internal bool RemoveSymbolUi(SymbolUi newContainerUi) { var symbolId = newContainerUi.Symbol.Id; return SymbolUiDict.TryRemove(symbolId, out _) && SymbolDict.TryRemove(symbolId, out _); } protected sealed override void OnSymbolsLoaded() { var assemblyInformation = AssemblyInformation; //var types = assemblyInformation.TypesInheritingFrom(typeof(IEditorUiExtension)).ToArray(); // register descriptive UI // TODO: Implement this // foreach (var operatorInfo in assemblyInformation.OperatorTypeInfo.Values) // { // if (operatorInfo.IsDescriptiveFileNameType) // { // CustomChildUiRegistry.Register(operatorInfo.Type, DescriptiveUi.DrawChildUiDelegate, _descriptiveUiTypes); // } // } // load ui initializers // foreach (var type in types) // { // var activated = assemblyInformation.CreateInstance(type); // if (activated == null) // { // Log.Error($"Created null object for {type.Name}"); // continue; // } // // // var extension = (IEditorUiExtension)activated; // // try // // { // // extension.Initialize(); // // } // // catch (Exception e) // // { // // Log.Error($"Failed to initialize UI extension {type.Name}: {e}"); // // continue; // // } // // // Log.Info($"Loaded UI initializer for {assemblyInformation.Name}: {type.Name}"); // // _extensions.Add(extension); // } // if (_extensions.Count != 0 && assemblyInformation.OperatorTypeInfo.Count > 0) // { // BlockingWindow.Instance.ShowMessageBox("Custom UI extensions are not supported in projects that also have symbols defined. " + // "This may cause issues with exporting. It is recommended to start a new C# project for custom UIs.", // "Warning"); // } } public bool NeedsAssemblyLoad { get; private set; } = true; // remove all possible references to the assembly private void OnAssemblyUnloaded(AssemblyInformation asm) { UnloadInProgress = true; try { AssemblyUnloading?.Invoke(); } catch (Exception e) { Log.Error($"Failed to unload assembly {asm.Name}: {e}"); } Log.Info("Unloading assembly " + asm.Name); //remove type information from symbols // it is critical that all references to the assembly are removed to allow the assembly to be unloaded // this includes all instances foreach (var symbol in SymbolDict.Values) { symbol.RemoveAllReferencesToType(); } // unload custom UIs // for (var index = _extensions.Count - 1; index >= 0; index--) // { // var extension = _extensions[index]; // _extensions.RemoveAt(index); // try // { // extension.Uninitialize(); // } // catch (Exception e) // { // Log.Error($"Failed to uninitialize UI extension {extension.GetType().Name}: {e}"); // } // } // unload descriptive uis // FIXME: implement this. // for (var index = _descriptiveUiTypes.Count - 1; index >= 0; index--) // { // var type = _descriptiveUiTypes[index]; // CustomChildUiRegistry.Remove(type, _descriptiveUiTypes); // } NeedsAssemblyLoad = true; } protected bool UnloadInProgress { get; private set; } public event Action? AssemblyUnloading; private readonly List _descriptiveUiTypes = []; public static void NotifySymbolStructureChange() { SymbolStructureVersionCounter++; } public static int SymbolStructureVersionCounter { get; private set; } }