diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs index bd9e60424..8624189ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs @@ -4,7 +4,7 @@ namespace Barotrauma.LuaCs.Data; public partial interface IModConfigInfo : IStylesResourcesInfo { } -public interface IStylesResourceInfo : IResourceInfo, IResourceCultureInfo, IDataInfo, IPackageDependenciesInfo { } +public interface IStylesResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageInfo, IPackageDependenciesInfo { } public interface IStylesResourcesInfo { diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs index d1ceb600c..96c92093b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs @@ -12,7 +12,7 @@ public partial class PackageService : IStylesResourcesInfo public IStylesService Styles => _stylesService.Value; public PackageService( - Lazy configParserService, + Lazy configParserService, Lazy luaScriptService, Lazy localizationService, Lazy pluginService, diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs index e4d92ec2f..d64643ebb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs @@ -8,7 +8,7 @@ namespace Barotrauma.LuaCs.Services; public partial class PackageService { public PackageService( - Lazy configParserService, + Lazy configParserService, Lazy luaScriptService, Lazy localizationService, Lazy pluginService, diff --git a/Barotrauma/BarotraumaShared/Luatrauma.props b/Barotrauma/BarotraumaShared/Luatrauma.props index 1adac0339..5b7154571 100644 --- a/Barotrauma/BarotraumaShared/Luatrauma.props +++ b/Barotrauma/BarotraumaShared/Luatrauma.props @@ -8,7 +8,7 @@ - + diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs index 330b08ac4..34933f7d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs @@ -1,12 +1,15 @@ -namespace Barotrauma.LuaCs.Data; +using System; +using System.Collections.Generic; + +namespace Barotrauma.LuaCs.Data; /// /// Serves as a compound-key to refer to all resources and information that comes from a specific source. /// -public interface IDataInfo +public interface IDataInfo : IEqualityComparer, IEquatable { /// - /// Package-Unique name to be used internally for all representations of, and references to, this information. + /// Internal name unique within the resources inside a package. /// string InternalName { get; } /// @@ -17,4 +20,33 @@ public interface IDataInfo /// Used in place of the package data when the OwnerPackage is missing. /// string FallbackPackageName { get; } + + bool IEqualityComparer.Equals(IDataInfo x, IDataInfo y) + { + if (x is null || y is null) + return false; + if (x.OwnerPackage is null) + throw new NullReferenceException($"ContentPackage not set for resource {x}!"); + if (y.OwnerPackage is null) + throw new NullReferenceException($"ContentPackage not set for resource {y}!"); + if (x.InternalName.IsNullOrWhiteSpace()) + throw new NullReferenceException($"InternalName not set for resource {x}!"); + if (y.InternalName.IsNullOrWhiteSpace()) + throw new NullReferenceException($"InternalName not set for resource {y}!"); + return x.OwnerPackage == y.OwnerPackage && x.InternalName == y.InternalName; + } + + bool IEquatable.Equals(IDataInfo other) + { + return Equals(this, other); + } + + int IEqualityComparer.GetHashCode(IDataInfo obj) + { + if (obj.OwnerPackage is null) + throw new NullReferenceException($"ContentPackage not set for resource {obj}!"); + if (obj.InternalName.IsNullOrWhiteSpace()) + throw new NullReferenceException($"InternalName is null for object {obj}!"); + return obj.InternalName.GetHashCode() + obj.OwnerPackage.GetHashCode(); + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index 56a8536f7..232ea11bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -43,7 +43,7 @@ namespace Barotrauma RegisterServices(); // load manifest - if (!_servicesProvider.TryGetService(out IModConfigParserService modConfigSvc)) + if (!_servicesProvider.TryGetService(out IModConfigCreatorService modConfigSvc)) throw new NullReferenceException("LuaCsSetup: Failed to get mod config parser service!"); // we should crash here var luaConfig = modConfigSvc.BuildConfigFromManifest(Directory.GetCurrentDirectory() + LuaCsConfigFile); if (!luaConfig.IsSuccess) @@ -421,7 +421,7 @@ namespace Barotrauma #endif } - + */ public void Stop() { @@ -439,7 +439,7 @@ namespace Barotrauma IsInitialized = true; Logger.Log($"Initializing LuaCs, git revision = {AssemblyInfo.GitRevision}"); - }*/ + } public void Update() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs index e903b0f9a..981c74bad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs @@ -18,6 +18,7 @@ using Microsoft.Xna.Framework; using OneOf; using Platform = Barotrauma.LuaCs.Data.Platform; +// This file is cursed, we put everything in it, and I'm not sorry about it. namespace Barotrauma.LuaCs { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs deleted file mode 100644 index e2017fbee..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs +++ /dev/null @@ -1,759 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.Loader; -using System.Threading; -using FluentResults; -using FluentResults.LuaCs; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; - -// ReSharper disable EventNeverSubscribedTo.Global -// ReSharper disable InconsistentNaming - -namespace Barotrauma.LuaCs.Services; - -/*** - * Note: This class was written to be thread-safe in order to allow parallelization in loading in the future if the need - * becomes necessary as there is almost no serial performance overhead for adding threading protection. - */ - -/// -/// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin. -/// All plugins are loaded into their own AssemblyLoadContext along with their dependencies. -/// -[Obsolete] -public class AssemblyManager : IAssemblyManagementService, IPluginManagementService -{ - #region ExternalAPI - - public event Action OnAssemblyLoaded; - public event Action OnAssemblyUnloading; - public event Action OnException; - public event Action OnACLUnload; - public ImmutableList> StillUnloadingACLs - { - get - { - OpsLockUnloaded.EnterReadLock(); - try - { - return UnloadingACLs.ToImmutableList(); - } - finally - { - OpsLockUnloaded.ExitReadLock(); - } - } - } - public bool IsCurrentlyUnloading - { - get - { - OpsLockUnloaded.EnterReadLock(); - try - { - return UnloadingACLs.Any(); - } - catch (Exception) - { - return false; - } - finally - { - OpsLockUnloaded.ExitReadLock(); - } - } - } - public IEnumerable GetSubTypesInLoadedAssemblies(bool rebuildList) - { - Type targetType = typeof(T); - string typeName = targetType.FullName ?? targetType.Name; - - // rebuild - if (rebuildList) - RebuildTypesList(); - - // check cache - if (_subTypesLookupCache.TryGetValue(typeName, out var subTypeList)) - { - return subTypeList; - } - - // build from scratch - OpsLockLoaded.EnterReadLock(); - try - { - // build list - var list1 = _defaultContextTypes - .Where(kvp1 => targetType.IsAssignableFrom(kvp1.Value) && !kvp1.Value.IsInterface) - .Concat(LoadedACLs - .SelectMany(kvp => kvp.Value.AssembliesTypes) - .Where(kvp2 => targetType.IsAssignableFrom(kvp2.Value) && !kvp2.Value.IsInterface)) - .Select(kvp3 => kvp3.Value) - .ToImmutableList(); - - // only add if we find something - if (list1.Count > 0) - { - if (!_subTypesLookupCache.TryAdd(typeName, list1)) - { - ModUtils.Logging.PrintError( - $"{nameof(AssemblyManager)}: Unable to add subtypes to cache of type {typeName}!"); - } - } - else - { - ModUtils.Logging.PrintMessage( - $"{nameof(AssemblyManager)}: Warning: No types found during search for subtypes of {typeName}"); - } - - return list1; - } - catch (Exception e) - { - this.OnException?.Invoke($"{nameof(AssemblyManager)}::{nameof(GetSubTypesInLoadedAssemblies)}() | Error: {e.Message}", e); - return ImmutableList.Empty; - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types) - { - Type targetType = typeof(T); - - if (TryGetACL(id, out var acl)) - { - types = acl.AssembliesTypes - .Where(kvp => targetType.IsAssignableFrom(kvp.Value) && !kvp.Value.IsInterface) - .Select(kvp => kvp.Value); - return true; - } - - types = null; - return false; - } - public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types) - { - if (TryGetACL(id, out var acl)) - { - types = acl.AssembliesTypes.Select(kvp => kvp.Value); - return true; - } - - types = null; - return false; - } - public IEnumerable GetTypesByName(string typeName) - { - List types = new(); - if (typeName.IsNullOrWhiteSpace()) - return types; - - bool byRef = false; - if (typeName.StartsWith("out ") || typeName.StartsWith("ref ")) - { - typeName = typeName.Remove(0, 4); - byRef = true; - } - - - TypesListHelper(); - if (types.Count > 0) - return types; - - // we couldn't find it, rebuild and try one more time - RebuildTypesList(); - TypesListHelper(); - - if (types.Count > 0) - return types; - - OpsLockLoaded.EnterReadLock(); - try - { - // fallback to Type.GetType - Type t = Type.GetType(typeName, false, false); - if (t is not null) - { - types.Add(byRef ? t.MakeByRefType() : t); - return types; - } - - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - try - { - t = assembly.GetType(typeName, false, false); - if (t is not null) - types.Add(byRef ? t.MakeByRefType() : t); - } - catch (Exception e) - { - this.OnException?.Invoke( - $"{nameof(AssemblyManager)}::{nameof(GetTypesByName)}() | Error: {e.Message}", e); - } - } - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - - return types; - - void TypesListHelper() - { - if (_defaultContextTypes.TryGetValue(typeName, out var type1)) - { - if (type1 is not null) - types.Add(byRef ? type1.MakeByRefType() : type1); - } - - OpsLockLoaded.EnterReadLock(); - try - { - foreach (KeyValuePair loadedAcl in LoadedACLs) - { - var at = loadedAcl.Value.AssembliesTypes; - if (at.TryGetValue(typeName, out var type2)) - { - if (type2 is not null) - types.Add(byRef ? type2.MakeByRefType() : type2); - } - } - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - } - public IEnumerable GetAllTypesInLoadedAssemblies() - { - OpsLockLoaded.EnterReadLock(); - try - { - return _defaultContextTypes - .Select(kvp => kvp.Value) - .Concat(LoadedACLs - .SelectMany(kvp => kvp.Value?.AssembliesTypes.Select(kv => kv.Value))) - .ToImmutableList(); - } - catch - { - return ImmutableList.Empty; - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - public IEnumerable GetAllLoadedACLs() - { - OpsLockLoaded.EnterReadLock(); - try - { - if (!LoadedACLs.Any()) - { - return ImmutableList.Empty; - } - - return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList(); - } - catch - { - return ImmutableList.Empty; - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - - public bool IsAssemblyLoadedGlobal(string friendlyName) - { - throw new NotImplementedException(); - } - - #endregion - - #region InternalAPI - - [MethodImpl(MethodImplOptions.Synchronized | MethodImplOptions.NoInlining)] - ImmutableList IAssemblyManagementService.UnsafeGetAllLoadedACLs() - { - if (LoadedACLs.IsEmpty) - return ImmutableList.Empty; - return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList(); - } - public event System.Func IsReadyToUnloadACL; - public AssemblyLoadingSuccessState LoadAssemblyFromMemory([NotNull] string compiledAssemblyName, - [NotNull] IEnumerable syntaxTree, - IEnumerable externalMetadataReferences, - [NotNull] CSharpCompilationOptions compilationOptions, - string friendlyName, - ref Guid id, - IEnumerable externFileAssemblyRefs = null) - { - // validation - if (compiledAssemblyName.IsNullOrWhiteSpace()) - return AssemblyLoadingSuccessState.BadName; - - if (syntaxTree is null) - return AssemblyLoadingSuccessState.InvalidAssembly; - - if (!GetOrCreateACL(id, friendlyName, out var acl)) - return AssemblyLoadingSuccessState.ACLLoadFailure; - - id = acl.Id; // pass on true id returned - - // this acl is already hosting an in-memory assembly - if (acl.Acl.CompiledAssembly is not null) - return AssemblyLoadingSuccessState.AlreadyLoaded; - - // compile - AssemblyLoadingSuccessState state; - string messages; - try - { - state = acl.Acl.CompileAndLoadScriptAssembly(compiledAssemblyName, syntaxTree, externalMetadataReferences, - compilationOptions, out messages, externFileAssemblyRefs); - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}::{nameof(LoadAssemblyFromMemory)}() | Failed to compile and load assemblies for [ {compiledAssemblyName} / {friendlyName} ]! Details: {e.Message} | {e.StackTrace}"); - return AssemblyLoadingSuccessState.InvalidAssembly; - } - - // get types - if (state is AssemblyLoadingSuccessState.Success) - { - _subTypesLookupCache.Clear(); - acl.RebuildTypesList(); - OnAssemblyLoaded?.Invoke(acl.Acl.CompiledAssembly); - } - else - { - ModUtils.Logging.PrintError($"Unable to compile assembly '{compiledAssemblyName}' due to errors: {messages}"); - } - - return state; - } - public bool SetACLToTemplateMode(Guid guid) - { - if (!TryGetACL(guid, out var acl)) - return false; - acl.Acl.IsTemplateMode = true; - return true; - } - public AssemblyLoadingSuccessState LoadAssembliesFromLocations([NotNull] IEnumerable filePaths, - string friendlyName, ref Guid id) - { - - if (filePaths is null) - { - var exception = new ArgumentNullException( - $"{nameof(AssemblyManager)}::{nameof(LoadAssembliesFromLocations)}() | file paths supplied is null!"); - this.OnException?.Invoke($"Error: {exception.Message}", exception); - throw exception; - } - - ImmutableList assemblyFilePaths = filePaths.ToImmutableList(); // copy the list before loading - - if (!assemblyFilePaths.Any()) - { - return AssemblyLoadingSuccessState.NoAssemblyFound; - } - - if (GetOrCreateACL(id, friendlyName, out var loadedAcl)) - { - var state = loadedAcl.Acl.LoadFromFiles(assemblyFilePaths); - // if failure, we dispose of the acl - if (state != AssemblyLoadingSuccessState.Success) - { - DisposeACL(loadedAcl.Id); - ModUtils.Logging.PrintError($"ACL {friendlyName} failed, unloading..."); - return state; - } - // build types list - _subTypesLookupCache.Clear(); - loadedAcl.RebuildTypesList(); - id = loadedAcl.Id; - foreach (Assembly assembly in loadedAcl.Acl.Assemblies) - { - OnAssemblyLoaded?.Invoke(assembly); - } - return state; - } - - return AssemblyLoadingSuccessState.ACLLoadFailure; - } - [MethodImpl(MethodImplOptions.NoInlining)] - public bool TryBeginDispose() - { - OpsLockLoaded.EnterWriteLock(); - OpsLockUnloaded.EnterWriteLock(); - try - { - _subTypesLookupCache.Clear(); - _defaultContextTypes = _defaultContextTypes.Clear(); - - foreach (KeyValuePair loadedAcl in LoadedACLs) - { - if (loadedAcl.Value.Acl is not null) - { - if (IsReadyToUnloadACL is not null) - { - foreach (Delegate del in IsReadyToUnloadACL.GetInvocationList()) - { - if (del is System.Func { } func) - { - if (!func.Invoke(loadedAcl.Value)) - return false; // Not ready, exit - } - } - } - - foreach (Assembly assembly in loadedAcl.Value.Acl.Assemblies) - { - OnAssemblyUnloading?.Invoke(assembly); - } - - UnloadingACLs.Add(new WeakReference(loadedAcl.Value.Acl, true)); - loadedAcl.Value.ClearTypesList(); - loadedAcl.Value.Acl.Unload(); - loadedAcl.Value.ClearACLRef(); - OnACLUnload?.Invoke(loadedAcl.Value.Id); - } - } - - LoadedACLs.Clear(); - return true; - } - catch(Exception e) - { - // should never happen - this.OnException?.Invoke($"{nameof(TryBeginDispose)}() | Error: {e.Message}", e); - return false; - } - finally - { - OpsLockUnloaded.ExitWriteLock(); - OpsLockLoaded.ExitWriteLock(); - } - } - [MethodImpl(MethodImplOptions.NoInlining)] - public bool FinalizeDispose() - { - bool isUnloaded; - OpsLockUnloaded.EnterUpgradeableReadLock(); - try - { - GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); // force the gc to collect unloaded acls. - List> toRemove = new(); - foreach (WeakReference weakReference in UnloadingACLs) - { - if (!weakReference.TryGetTarget(out _)) - { - toRemove.Add(weakReference); - } - } - - if (toRemove.Any()) - { - OpsLockUnloaded.EnterWriteLock(); - try - { - foreach (WeakReference reference in toRemove) - { - UnloadingACLs.Remove(reference); - } - } - finally - { - OpsLockUnloaded.ExitWriteLock(); - } - } - isUnloaded = !UnloadingACLs.Any(); - } - finally - { - OpsLockUnloaded.ExitUpgradeableReadLock(); - } - - return isUnloaded; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public bool TryGetACL(Guid id, out LoadedACL acl) - { - acl = null; - OpsLockLoaded.EnterReadLock(); - try - { - if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id)) - return false; - acl = LoadedACLs[id]; - return true; - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - - - /// - /// Gets or creates an AssemblyCtxLoader for the given ID. Creates if the ID is empty or no ACL can be found. - /// [IMPORTANT] After calling this method, the id you use should be taken from the acl container (acl.Id). - /// - /// - /// A non-unique name for later reference. Optional. - /// - /// Should only return false if an error occurs. - [MethodImpl(MethodImplOptions.NoInlining)] - private bool GetOrCreateACL(Guid id, string friendlyName, out LoadedACL acl) - { - OpsLockLoaded.EnterUpgradeableReadLock(); - try - { - if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id) || LoadedACLs[id] is null) - { - OpsLockLoaded.EnterWriteLock(); - try - { - id = Guid.NewGuid(); - acl = new LoadedACL(id, this, friendlyName); - LoadedACLs[id] = acl; - return true; - } - finally - { - OpsLockLoaded.ExitWriteLock(); - } - } - else - { - acl = LoadedACLs[id]; - return true; - } - - } - catch(Exception e) - { - this.OnException?.Invoke($"{nameof(GetOrCreateACL)}Error: {e.Message}", e); - acl = null; - return false; - } - finally - { - OpsLockLoaded.ExitUpgradeableReadLock(); - } - } - - - [MethodImpl(MethodImplOptions.NoInlining)] - private bool DisposeACL(Guid id) - { - OpsLockLoaded.EnterWriteLock(); - OpsLockUnloaded.EnterWriteLock(); - try - { - if (LoadedACLs.ContainsKey(id) && LoadedACLs[id] == null) - { - if (!LoadedACLs.TryRemove(id, out _)) - { - ModUtils.Logging.PrintWarning($"An ACL with the GUID {id.ToString()} was found as null. Unable to remove null ACL entry."); - } - } - - if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id)) - { - return false; // nothing to dispose of - } - - var acl = LoadedACLs[id]; - - foreach (Assembly assembly in acl.Acl.Assemblies) - { - OnAssemblyUnloading?.Invoke(assembly); - } - - _subTypesLookupCache.Clear(); - UnloadingACLs.Add(new WeakReference(acl.Acl, true)); - acl.Acl.Unload(); - acl.ClearACLRef(); - OnACLUnload?.Invoke(acl.Id); - - return true; - } - catch (Exception e) - { - this.OnException?.Invoke($"{nameof(DisposeACL)}() | Error: {e.Message}", e); - return false; - } - finally - { - OpsLockLoaded.ExitWriteLock(); - OpsLockUnloaded.ExitWriteLock(); - } - } - - internal AssemblyManager() - { - RebuildTypesList(); - } - - /// - /// Rebuilds the list of types in the default assembly load context. - /// - private void RebuildTypesList() - { - try - { - _defaultContextTypes = AssemblyLoadContext.Default.Assemblies - .SelectMany(a => a.GetSafeTypes()) - .ToImmutableDictionary(t => t.FullName ?? t.Name, t => t); - _subTypesLookupCache.Clear(); - } - catch(ArgumentException ae) - { - this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {ae.Message}", ae); - try - { - // some types must've had duplicate type names, build the list while filtering - Dictionary types = new(); - foreach (var type in AssemblyLoadContext.Default.Assemblies.SelectMany(a => a.GetSafeTypes())) - { - try - { - types.TryAdd(type.FullName ?? type.Name, type); - } - catch - { - // ignore, null key exception - } - } - - _defaultContextTypes = types.ToImmutableDictionary(); - } - catch (Exception e) - { - this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {e.Message}", e); - ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Unable to create list of default assembly types! Default AssemblyLoadContext types searching not available."); -#if DEBUG - ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Exception Details :{e.Message} | {e.InnerException}"); -#endif - _defaultContextTypes = ImmutableDictionary.Empty; - } - } - } - - #endregion - - #region Data - - private readonly ConcurrentDictionary> _subTypesLookupCache = new(); - private ImmutableDictionary _defaultContextTypes; - private readonly ConcurrentDictionary LoadedACLs = new(); - private readonly List> UnloadingACLs= new(); - private readonly ReaderWriterLockSlim OpsLockLoaded = new (); - private readonly ReaderWriterLockSlim OpsLockUnloaded = new (); - - #endregion - - #region TypeDefs - - - public sealed class LoadedACL - { - public readonly Guid Id; - private ImmutableDictionary _assembliesTypes = ImmutableDictionary.Empty; - public MemoryFileAssemblyContextLoader Acl { get; private set; } - - internal LoadedACL(Guid id, AssemblyManager manager, string friendlyName) - { - this.Id = id; - this.Acl = new(manager) - { - FriendlyName = friendlyName - }; - } - public ref readonly ImmutableDictionary AssembliesTypes => ref _assembliesTypes; - - /// - /// Warning: For use by the Assembly Manager only! Do not call this method otherwise. - /// - internal void ClearACLRef() - { - Acl = null; - } - - /// - /// Rebuild the list of types from assemblies loaded in the AsmCtxLoader. - /// - internal void RebuildTypesList() - { - if (this.Acl is null) - { - ModUtils.Logging.PrintWarning($"{nameof(RebuildTypesList)}() | ACL with GUID {Id.ToString()} is null, cannot rebuild."); - return; - } - - ClearTypesList(); - try - { - _assembliesTypes = this.Acl.Assemblies - .SelectMany(a => a.GetSafeTypes()) - .ToImmutableDictionary(t => t.FullName ?? t.Name, t => t); - } - catch(ArgumentException) - { - // some types must've had duplicate type names, build the list while filtering - Dictionary types = new(); - foreach (var type in this.Acl.Assemblies.SelectMany(a => a.GetSafeTypes())) - { - try - { - types.TryAdd(type.FullName ?? type.Name, type); - } - catch - { - // ignore, null key exception - } - } - - _assembliesTypes = types.ToImmutableDictionary(); - } - } - - internal void ClearTypesList() - { - _assembliesTypes = ImmutableDictionary.Empty; - } - } - - #endregion - - public void Dispose() - { - TryBeginDispose(); - } - - public FluentResults.Result Reset() - { - return TryBeginDispose() ? FluentResults.Result.Ok() - : FluentResults.Result.Fail(new Error($"{nameof(AssemblyManager)}: failed to Reset service.") - .WithMetadata(MetadataType.ExceptionObject, this)); - } -} - - diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs index 219685d5b..83f133fc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs @@ -23,7 +23,7 @@ public partial class PackageService : IPackageService // mod config / package scanners/parsers - private readonly Lazy _configParserService; + private readonly Lazy _configParserService; private readonly Lazy _luaScriptService; private readonly Lazy _localizationService; private readonly Lazy _pluginService; diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs index 31822e438..6f46b99e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs @@ -1,4 +1,14 @@ -using System.Collections.Immutable; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using System.Threading; +using Barotrauma.Extensions; using Barotrauma.LuaCs.Data; using FluentResults; using Microsoft.CodeAnalysis; @@ -7,5 +17,152 @@ namespace Barotrauma.LuaCs.Services; public class PluginManagementService : IPluginManagementService, IAssemblyManagementService { + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + private int _isDisposed; + private readonly ReaderWriterLockSlim _operationsLock = new(LockRecursionPolicy.SupportsRecursion); + + private readonly ConcurrentDictionary _assemblyServices = new(); + private readonly ConcurrentDictionary _resourceData = new(); + private readonly Lazy _eventService; + private readonly Func _assemblyServiceFactory; + private ImmutableDictionary _cachedTypes = null; + private ImmutableDictionary DefaultTypeCache => _cachedTypes ??= AssemblyLoadContext.Default.Assemblies + .SelectMany(ass => ass.GetSafeTypes()).ToImmutableDictionary(type => type.FullName, type => type); + + + public bool IsResourceLoaded(T resource) where T : IAssemblyResourceInfo + { + ((IService)this).CheckDisposed(); + return _resourceData.ContainsKey(resource); + } + + public Result> GetImplementingTypes(string namespacePrefix = null, bool includeInterfaces = false, + bool includeAbstractTypes = false, bool includeDefaultContext = true) + { + ((IService)this).CheckDisposed(); + var types = ImmutableArray.CreateBuilder(); + _operationsLock.EnterReadLock(); + try + { + if (AssemblyLoaderServices.Any()) + { + types.AddRange(AssemblyLoaderServices + .SelectMany(als => als.UnsafeGetTypesInAssemblies()) + .Where(t => t is not null) + .Where(type => typeof(T).IsAssignableFrom(type)) + .Where(type => includeInterfaces || !type.IsInterface) + .Where(type => includeAbstractTypes || !type.IsAbstract) + .Where(type => namespacePrefix is not null && type.FullName is not null && type.FullName.StartsWith(namespacePrefix))); + } + + if (includeDefaultContext) + { + types.AddRange(AssemblyLoadContext.Default.Assemblies + .SelectMany(ass => ass.GetSafeTypes()) + .Where(t => t is not null) + .Where(type => typeof(T).IsAssignableFrom(type)) + .Where(type => includeInterfaces || !type.IsInterface) + .Where(type => includeAbstractTypes || !type.IsAbstract) + .Where(type => namespacePrefix is not null && type.FullName is not null && type.FullName.StartsWith(namespacePrefix))); + } + + return types.MoveToImmutable(); + } + finally + { + _operationsLock.ExitReadLock(); + } + } + + public Type GetType(string typeName) + { + ((IService)this).CheckDisposed(); + _operationsLock.EnterReadLock(); + try + { + if (DefaultTypeCache.TryGetValue(typeName, out var type)) + return type; + if (AssemblyLoaderServices.None()) + return null; + foreach (var loaderService in AssemblyLoaderServices) + { + if (loaderService.GetTypeInAssemblies(typeName) is { IsSuccess: true, Value: not null } ret) + return ret.Value; + } + return null; + } + finally + { + _operationsLock.ExitReadLock(); + } + } + + public Result> LoadAssemblyResources(ImmutableArray resource) + { + throw new NotImplementedException(); + } + + public Result GetLoadedAssembly(string assemblyName, in Guid[] excludedContexts) + { + ((IService)this).CheckDisposed(); + _operationsLock.EnterReadLock(); + try + { + foreach (var (guid, context) in _assemblyServices) + { + if (excludedContexts.Length > 0 && excludedContexts.Contains(guid)) + continue; + if (context.GetAssemblyByName(assemblyName) is { IsSuccess: true, Value: not null } ret) + return ret.Value; + } + return FluentResults.Result.Fail($"Could not find assembly {assemblyName}"); + } + finally + { + _operationsLock.ExitReadLock(); + } + } + + public Result GetLoadedAssembly(AssemblyName assemblyName, in Guid[] excludedContexts) + => GetLoadedAssembly(assemblyName.FullName, excludedContexts); + + public ImmutableArray GetDefaultMetadataReferences() => + Basic.Reference.Assemblies.Net60.References.All.Select(Unsafe.As).ToImmutableArray(); + + public ImmutableArray GetAddInContextsMetadataReferences() + { + ((IService)this).CheckDisposed(); + _operationsLock.EnterReadLock(); + try + { + if (_assemblyServices.IsEmpty) + return ImmutableArray.Empty; + var builder = ImmutableArray.CreateBuilder(); + foreach (var context in _assemblyServices.Values) + builder.AddRange(context.AssemblyReferences); + return builder.ToImmutable(); + } + finally + { + _operationsLock.ExitReadLock(); + } + } + + public ImmutableArray AssemblyLoaderServices { get; } + + public void Dispose() + { + // TODO release managed resources here + throw new NotImplementedException(); + } + + public FluentResults.Result Reset() + { + throw new NotImplementedException(); + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IModConfigParserService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IModConfigCreatorService.cs similarity index 81% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IModConfigParserService.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IModConfigCreatorService.cs index e90f4d598..f91bdba34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IModConfigParserService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IModConfigCreatorService.cs @@ -2,7 +2,7 @@ namespace Barotrauma.LuaCs.Services.Processing; -public interface IModConfigParserService : IReusableService +public interface IModConfigCreatorService : IService { FluentResults.Result BuildConfigForPackage(ContentPackage package); FluentResults.Result BuildConfigFromManifest(string manifestPath); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs index 0a06c13b9..d81ef4dac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs @@ -18,28 +18,24 @@ public interface IPluginManagementService : IReusableService /// /// /// - /// /// /// /// /// - /// /// /// - FluentResults.Result> GetTypes( - ContentPackage package = null, + FluentResults.Result> GetImplementingTypes( string namespacePrefix = null, bool includeInterfaces = false, bool includeAbstractTypes = false, - bool includeDefaultContext = true, - bool includeExplicitAssembliesOnly = false); - + bool includeDefaultContext = true); + /// - /// + /// Tries to get the /// - /// + /// /// - FluentResults.Result> GetCachedAssembliesForPackage(ContentPackage package); + Type GetType(string typeName); /// /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IServicesProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IServicesProvider.cs index 8cffa329f..40dd31e6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IServicesProvider.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IServicesProvider.cs @@ -19,7 +19,7 @@ public interface IServicesProvider /// /// /// - void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IReusableService where TService : class, IReusableService, TSvcInterface; + void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface; /// /// Registers a type as a service for a given interface that can be requested by name. @@ -29,7 +29,7 @@ public interface IServicesProvider /// /// /// - void RegisterServiceType(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IReusableService where TService : class, IReusableService, TSvcInterface; + void RegisterServiceType(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface; /// /// Called whenever a new service type for a given interface is implemented. @@ -58,27 +58,25 @@ public interface IServicesProvider /// Tries to get a service for the given interface, returns success/failure. /// /// - /// /// /// - bool TryGetService(out TSvcInterface service) where TSvcInterface : class, IReusableService; + bool TryGetService(out TSvcInterface service) where TSvcInterface : class, IService; /// /// Tries to get a service for the given name and interface, returns success/failure. /// /// /// - /// /// /// - bool TryGetService(string name, out TSvcInterface service) where TSvcInterface : class, IReusableService; + bool TryGetService(string name, out TSvcInterface service) where TSvcInterface : class, IService; /// /// Called whenever a new service is created/instanced. /// Args[0]: The interface type of the service. /// Args[1]: The instance of the service. /// - event System.Action OnServiceInstanced; + event System.Action OnServiceInstanced; #endregion @@ -89,7 +87,7 @@ public interface IServicesProvider /// /// /// - ImmutableArray GetAllServices() where TSvc : class, IReusableService; + ImmutableArray GetAllServices() where TSvc : class, IService; #endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs index 0d132fcdd..bbcac52a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs @@ -7,8 +7,7 @@ namespace Barotrauma.LuaCs.Services; public interface IStorageService : IService { - #region LocalGameData - + // -- local game folder storage FluentResults.Result LoadLocalXml(ContentPackage package, string localFilePath); FluentResults.Result LoadLocalBinary(ContentPackage package, string localFilePath); FluentResults.Result LoadLocalText(ContentPackage package, string localFilePath); @@ -22,10 +21,8 @@ public interface IStorageService : IService Task SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document); Task SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes); Task SaveLocalTextAsync(ContentPackage package, string localFilePath, string text); - - #endregion - #region ContentPackageData + // -- package directory // singles FluentResults.Result LoadPackageXml(ContentPackage package, string localFilePath); FluentResults.Result LoadPackageBinary(ContentPackage package, string localFilePath); @@ -45,9 +42,7 @@ public interface IStorageService : IService Task)>> LoadPackageBinaryFilesAsync(ContentPackage package, ImmutableArray localFilePaths); Task)>> LoadPackageTextFilesAsync(ContentPackage package, ImmutableArray localFilePaths); - #endregion - - #region AbsolutePaths + // -- absolute paths FluentResults.Result TryLoadXml(string filePath, Encoding encoding = null); FluentResults.Result TryLoadText(string filePath, Encoding encoding = null); FluentResults.Result TryLoadBinary(string filePath); @@ -62,5 +57,4 @@ public interface IStorageService : IService Task TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null); Task TrySaveTextAsync(string filePath, string text, Encoding encoding = null); Task TrySaveBinaryAsync(string filePath, byte[] bytes); - #endregion } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs index 1dea21e4a..ebfc6269e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs @@ -3,23 +3,20 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Dynamic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; using System.Threading; +using Barotrauma.Extensions; using Barotrauma.LuaCs; -using Barotrauma.LuaCs.Events; using Microsoft.CodeAnalysis; -using Basic.Reference.Assemblies; using FluentResults; using FluentResults.LuaCs; -using LightInject; using Microsoft.CodeAnalysis.CSharp; using OneOf; -using Path = Barotrauma.IO.Path; +using Path = System.IO.Path; [assembly: InternalsVisibleTo(IAssemblyLoaderService.InternalsAwareAssemblyName)] @@ -34,59 +31,92 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService private set => ModUtils.Threading.SetBool(ref _isDisposed, value); } private int _isDisposed; + + /// + /// This bool-int wrapper increments/decrements when set as true/false respectively and return true if the value > 0. + /// + private bool AreOperationRunning + { + get => Interlocked.CompareExchange(ref _operationsRunning, 0, 0) > 0; + set // we use the set as our inc/decr + { + if (value) + { + Interlocked.Add(ref _operationsRunning, 1); + } + else + { + Interlocked.Add(ref _operationsRunning, -1); + } + } + } + private int _operationsRunning; //internal private readonly IAssemblyManagementService _assemblyManagementService; - private readonly IEventService _eventService; private readonly Action _onUnload; - /// - /// This lock is just to ensure that we do not load while disposing - /// - private readonly ReaderWriterLockSlim _operationsLock = new(LockRecursionPolicy.SupportsRecursion); private readonly ConcurrentDictionary _dependencyResolvers = new(); private readonly ConcurrentDictionary _loadedAssemblyData = new(); - private ThreadLocal _isResolving = new(static()=>false); // cyclic resolution exit - - #region PublicAPI + private readonly ThreadLocal _isResolving = new(static()=>false); // cyclic resolution exit public AssemblyLoader(IAssemblyManagementService assemblyManagementService, - IEventService eventService, Guid id, string name, bool isReferenceOnlyMode, Action onUnload) : base(isCollectible: true, name: name) { _assemblyManagementService = assemblyManagementService; - _eventService = eventService; Id = id; IsReferenceOnlyMode = isReferenceOnlyMode; _onUnload = onUnload; if (_onUnload is not null) - { base.Unloading += OnUnload; - } - + } + + public IEnumerable AssemblyReferences + { + get + { + if (IsDisposed || _loadedAssemblyData.IsEmpty) + yield return null; + AreOperationRunning = true; + foreach (var data in _loadedAssemblyData.Values) + { + yield return data.AssemblyReference; + } + AreOperationRunning = false; + } } public FluentResults.Result AddDependencyPaths(ImmutableArray paths) { - if (paths.Length == 0) - return FluentResults.Result.Ok(); - var res = new FluentResults.Result(); - foreach (var path in paths) + if (IsDisposed) + return FluentResults.Result.Fail($"Loader is disposed!"); + AreOperationRunning = true; + try { - try + if (paths.Length == 0) + return FluentResults.Result.Ok(); + var res = new FluentResults.Result(); + foreach (var path in paths) { - var p = Path.GetFullPath(path.CleanUpPath()); - _dependencyResolvers[p] = new AssemblyDependencyResolver(p); - } - catch (Exception ex) - { - return res.WithError(new ExceptionalError(ex) - .WithMetadata(MetadataType.Sources, path)); + try + { + var p = Path.GetFullPath(path.CleanUpPath()); + _dependencyResolvers[p] = new AssemblyDependencyResolver(p); + } + catch (Exception ex) + { + return res.WithError(new ExceptionalError(ex) + .WithMetadata(MetadataType.Sources, path)); + } } + return FluentResults.Result.Ok(); + } + finally + { + AreOperationRunning = false; } - return FluentResults.Result.Ok(); } public FluentResults.Result CompileScriptAssembly( @@ -96,209 +126,303 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService ImmutableArray metadataReferences, CSharpCompilationOptions compilationOptions = null) { - if (assemblyName.IsNullOrWhiteSpace()) - { - return new FluentResults.Result().WithError(new Error($"The name provided is null!") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, syntaxTrees)); - } - - if (_loadedAssemblyData.ContainsKey(assemblyName)) - { - return new FluentResults.Result().WithError(new Error($"The name provided is already assigned to an assembly!") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, syntaxTrees)); - } - - var compilationAssemblyName = compileWithInternalAccess ? IAssemblyLoaderService.InternalsAwareAssemblyName : assemblyName; - - compilationOptions ??= new CSharpCompilationOptions( - outputKind: OutputKind.DynamicallyLinkedLibrary, - optimizationLevel: OptimizationLevel.Release, - concurrentBuild: true, - reportSuppressedDiagnostics: true, - allowUnsafe: true); - - if (!compileWithInternalAccess) - { - typeof(CSharpCompilationOptions) - .GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic) - ?.SetValue(compilationOptions, (uint)1 << 22); - } - - using var asmMemoryStream = new MemoryStream(); - var result = CSharpCompilation.Create(compilationAssemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(asmMemoryStream); - if (!result.Success) - { - var res = new FluentResults.Result().WithError( - new Error($"Compilation failed for assembly {assemblyName}!")); - var failuresDiag = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error); - foreach (var diag in failuresDiag) - { - res = res.WithError(new Error(diag.GetMessage()) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.ExceptionDetails, diag.Descriptor.Description)); - } - return res; - } - - asmMemoryStream.Seek(0, SeekOrigin.Begin); + if (IsDisposed) + return FluentResults.Result.Fail($"Loader is disposed!"); + AreOperationRunning = true; try { - var data = new AssemblyData(LoadFromStream(asmMemoryStream), asmMemoryStream.ToArray()); - _loadedAssemblyData[data.Assembly] = data; - return new FluentResults.Result().WithSuccess($"Compiled assembly {assemblyName} successful.").WithValue(data.Assembly); + if (assemblyName.IsNullOrWhiteSpace()) + { + return new Result().WithError(new Error($"The name provided is null!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, syntaxTrees)); + } + + if (_loadedAssemblyData.ContainsKey(assemblyName)) + { + return new Result().WithError(new Error($"The name provided is already assigned to an assembly!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, syntaxTrees)); + } + + var compilationAssemblyName = compileWithInternalAccess ? IAssemblyLoaderService.InternalsAwareAssemblyName : assemblyName; + + compilationOptions ??= new CSharpCompilationOptions( + outputKind: OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release, + concurrentBuild: true, + reportSuppressedDiagnostics: true, + allowUnsafe: true); + + if (!compileWithInternalAccess) + { + typeof(CSharpCompilationOptions) + .GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic) + ?.SetValue(compilationOptions, + (uint)1 << 25 // CSharp.BinderFlags.AllowAwaitInUnsafeContext + | (uint)1 << 22 // CSharp.BinderFlags.IgnoreAccessibility + | (uint)1 << 1 // CSharp.BinderFlags.SuppressObsoleteChecks + ); + } + + using var asmMemoryStream = new MemoryStream(); + var result = CSharpCompilation.Create(compilationAssemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(asmMemoryStream); + if (!result.Success) + { + var res = new FluentResults.Result().WithError( + new Error($"Compilation failed for assembly {assemblyName}!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, syntaxTrees)); + var failuresDiag = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error); + foreach (var diag in failuresDiag) + { + res = res.WithError(new Error(diag.GetMessage()) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.ExceptionDetails, diag.Descriptor.Description)); + } + return res; + } + + asmMemoryStream.Seek(0, SeekOrigin.Begin); + try + { + var data = new AssemblyData(LoadFromStream(asmMemoryStream), asmMemoryStream.ToArray()); + _loadedAssemblyData[data.Assembly] = data; + return new Result().WithSuccess($"Compiled assembly {assemblyName} successful.").WithValue(data.Assembly); + } + catch (Exception ex) + { + return new FluentResults.Result().WithError(new ExceptionalError(ex)); + } } - catch (Exception ex) + finally { - return new FluentResults.Result().WithError(new ExceptionalError(ex)); + AreOperationRunning = false; } } public FluentResults.Result LoadAssemblyFromFile(string assemblyFilePath, ImmutableArray additionalDependencyPaths) { - if (assemblyFilePath.IsNullOrWhiteSpace()) - return new FluentResults.Result().WithError(new Error($"The path provided is null!")); - - if (additionalDependencyPaths.Any()) - { - var r = AddDependencyPaths(additionalDependencyPaths); - if (!r.IsFailed) - { - // we have errors, loading may not work. - return FluentResults.Result.Fail(new Error($"Failed to load dependency paths") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath)) - .WithErrors(r.Errors); - } - } - - string sanitizedFilePath = Path.GetFullPath(assemblyFilePath.CleanUpPath()); - string directoryKey = Path.GetDirectoryName(sanitizedFilePath); - - if (directoryKey is null) - { - return FluentResults.Result.Fail(new Error($"Unable to load assembly: bath file path: {assemblyFilePath}") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, sanitizedFilePath)); - } + if (IsDisposed) + return FluentResults.Result.Fail($"Loader is disposed!"); + AreOperationRunning = true; try { - var assembly = LoadFromAssemblyPath(sanitizedFilePath); - _loadedAssemblyData[assembly] = new AssemblyData(assembly, sanitizedFilePath); - return new Result().WithSuccess($"Loaded assembly'{assembly.GetName()}'").WithValue(assembly); + if (assemblyFilePath.IsNullOrWhiteSpace()) + return new Result().WithError(new Error($"The path provided is null!")); + + if (additionalDependencyPaths.Any()) + { + var r = AddDependencyPaths(additionalDependencyPaths); + if (!r.IsFailed) + { + // we have errors, loading may not work. + return FluentResults.Result.Fail(new Error($"Failed to load dependency paths") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath)) + .WithErrors(r.Errors); + } + } + + string sanitizedFilePath = Path.GetFullPath(assemblyFilePath.CleanUpPath()); + string directoryKey = Path.GetDirectoryName(sanitizedFilePath); + + if (directoryKey is null) + { + return FluentResults.Result.Fail(new Error($"Unable to load assembly: bath file path: {assemblyFilePath}") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, sanitizedFilePath)); + } + + try + { + var assembly = LoadFromAssemblyPath(sanitizedFilePath); + _loadedAssemblyData[assembly] = new AssemblyData(assembly, sanitizedFilePath); + return new Result().WithSuccess($"Loaded assembly'{assembly.GetName()}'").WithValue(assembly); + } + catch (ArgumentNullException ane) + { + return FluentResults.Result.Fail(new ExceptionalError(ane) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, ane.Message) + .WithMetadata(MetadataType.StackTrace, ane.StackTrace)); + } + catch (ArgumentException ae) + { + return FluentResults.Result.Fail(new ExceptionalError(ae) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, ae.Message) + .WithMetadata(MetadataType.StackTrace, ae.StackTrace)); + } + catch (FileLoadException fle) + { + return FluentResults.Result.Fail(new ExceptionalError(fle) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, fle.Message) + .WithMetadata(MetadataType.StackTrace, fle.StackTrace)); + } + catch (FileNotFoundException fnfe) + { + return FluentResults.Result.Fail(new ExceptionalError(fnfe) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, fnfe.Message) + .WithMetadata(MetadataType.StackTrace, fnfe.StackTrace)); + } + catch (BadImageFormatException bife) + { + return FluentResults.Result.Fail(new ExceptionalError(bife) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, bife.Message) + .WithMetadata(MetadataType.StackTrace, bife.StackTrace)); + } + catch (Exception e) + { + return FluentResults.Result.Fail(new ExceptionalError(e) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, e.Message) + .WithMetadata(MetadataType.StackTrace, e.StackTrace)); + } } - catch (ArgumentNullException ane) + finally { - return FluentResults.Result.Fail(new ExceptionalError(ane) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, ane.Message) - .WithMetadata(MetadataType.StackTrace, ane.StackTrace)); - } - catch (ArgumentException ae) - { - return FluentResults.Result.Fail(new ExceptionalError(ae) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, ae.Message) - .WithMetadata(MetadataType.StackTrace, ae.StackTrace)); - } - catch (FileLoadException fle) - { - return FluentResults.Result.Fail(new ExceptionalError(fle) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, fle.Message) - .WithMetadata(MetadataType.StackTrace, fle.StackTrace)); - } - catch (FileNotFoundException fnfe) - { - return FluentResults.Result.Fail(new ExceptionalError(fnfe) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, fnfe.Message) - .WithMetadata(MetadataType.StackTrace, fnfe.StackTrace)); - } - catch (BadImageFormatException bife) - { - return FluentResults.Result.Fail(new ExceptionalError(bife) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, bife.Message) - .WithMetadata(MetadataType.StackTrace, bife.StackTrace)); - } - catch (Exception e) - { - return FluentResults.Result.Fail(new ExceptionalError(e) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, e.Message) - .WithMetadata(MetadataType.StackTrace, e.StackTrace)); + AreOperationRunning = false; } } public FluentResults.Result GetAssemblyByName(string assemblyName) { + if (IsDisposed) + return FluentResults.Result.Fail(new Error($"Loader is disposed!")); if (assemblyName.IsNullOrWhiteSpace()) { return FluentResults.Result.Fail(new Error($"Assembly name is null") .WithMetadata(MetadataType.ExceptionObject, this)); } - - if (_loadedAssemblyData.TryGetValue(assemblyName, out var data)) + AreOperationRunning = true; + try { - return new FluentResults.Result().WithSuccess(new Success($"Assembly found")).WithValue(data.Assembly); - } - - foreach (var assembly1 in this.Assemblies.Where(a => !_loadedAssemblyData.ContainsKey(a))) - { - if (assembly1.GetName().FullName == assemblyName) + if (_loadedAssemblyData.TryGetValue(assemblyName, out var data)) { - try - { - if (!assembly1.Location.IsNullOrWhiteSpace()) - { - _loadedAssemblyData[assembly1] = new AssemblyData(assembly1, assembly1.Location); - } - // we don't have the original byte array so we can't store it. - } - catch (NotSupportedException nse) // dynamic assembly or location property threw - { - // ignored - } - - return new FluentResults.Result().WithSuccess(new Success($"Assembly found")).WithValue(assembly1); + return new Result().WithSuccess(new Success($"Assembly found")).WithValue(data.Assembly); } - } - return FluentResults.Result.Fail(new Error($"Assembly named { assemblyName } not found!")); + // search any assemblies that were background loaded and we're unaware of. + foreach (var assembly1 in this.Assemblies.Where(a => !_loadedAssemblyData.ContainsKey(a))) + { + if (assembly1.GetName().FullName == assemblyName) + { + try + { + if (!assembly1.Location.IsNullOrWhiteSpace()) + { + _loadedAssemblyData[assembly1] = new AssemblyData(assembly1, assembly1.Location); + } + // we don't have the original byte array so we can't store it. + } + catch (NotSupportedException nse) // dynamic assembly or location property threw + { + // ignored + } + + return new Result().WithSuccess(new Success($"Assembly found")).WithValue(assembly1); + } + } + + return FluentResults.Result.Fail(new Error($"Assembly named { assemblyName } not found!")); + } + finally + { + AreOperationRunning = false; + } } public FluentResults.Result> GetTypesInAssemblies() { + if (IsDisposed) + return FluentResults.Result.Fail(new Error($"Loader is disposed!")); + AreOperationRunning = true; try { - return new FluentResults.Result>().WithValue(_loadedAssemblyData.SelectMany(kvp=> kvp.Value.Types).ToImmutableArray()); + return new FluentResults.Result>().WithValue(_loadedAssemblyData + .SelectMany(kvp => kvp.Value.Types).ToImmutableArray()); } catch (Exception e) { return FluentResults.Result.Fail(new ExceptionalError(e)); } + finally + { + AreOperationRunning = false; + } } - + + public IEnumerable UnsafeGetTypesInAssemblies() + { + if (IsDisposed) + yield return null; + AreOperationRunning = true; + try + { + if (_loadedAssemblyData.None()) + { + yield return null; + } + else + { + foreach (var assemblyData in _loadedAssemblyData.Values) + { + foreach (var type in assemblyData.Types) + { + yield return type; + } + } + } + } + finally + { + AreOperationRunning = false; + } + } + + public Result GetTypeInAssemblies(string typeName) + { + if (IsDisposed) + return FluentResults.Result.Fail(new Error($"Loader is disposed!")); + AreOperationRunning = true; + try + { + if (_loadedAssemblyData.IsEmpty) + return FluentResults.Result.Fail(new Error($"No assemblies loaded!")); + foreach (var assemblyData in _loadedAssemblyData) + { + if (assemblyData.Value.TypesByName.TryGetValue(typeName, out var type)) + return new FluentResults.Result().WithSuccess($"Found type.").WithValue(type); + } + return FluentResults.Result.Fail(new Error($"No matching types found for { typeName }!")); + } + finally + { + AreOperationRunning = false; + } + } + public void Dispose() { - Dispose(true); + if (IsDisposed) + return; // we don't want to invoke events twice nor cause strong GC handles. + IsDisposed = true; + this.Unload(); GC.SuppressFinalize(this); } - #endregion - - #region Internals - protected override Assembly Load(AssemblyName assemblyName) { if (_isResolving.Value) @@ -309,8 +433,8 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService { if (_loadedAssemblyData.TryGetValue(assemblyName.FullName, out var data)) return data.Assembly; - var idSpan = new[] { this.Id }; - if (_assemblyManagementService.GetLoadedAssembly(assemblyName, in idSpan) is { IsSuccess: true } ret) + var ids = new[] { this.Id }; + if (_assemblyManagementService.GetLoadedAssembly(assemblyName, in ids) is { IsSuccess: true } ret) return ret.Value; return null; } @@ -334,28 +458,22 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService private void OnUnload(AssemblyLoadContext context) { + IsDisposed = true; + + // Try to wait for loading ops on other threads if they happen to occur. + // Minor race condition on the loop exit but this loader is not intended to be thread-safe by design, this is just to cover edge cases. + DateTime timeout = DateTime.Now.AddSeconds(5); + while (timeout > DateTime.Now) + { + if (!AreOperationRunning) + break; + } + base.Unloading -= OnUnload; var wf = new WeakReference(this); - _eventService.PublishEvent((sub) => sub.OnAssemblyUnloading(wf)); _onUnload?.Invoke(this); - this.Dispose(true); - } - - private void Dispose(bool disposing) - { - if (ModUtils.Threading.CheckClearAndSetBool(ref _isDisposed)) - { - _operationsLock.EnterWriteLock(); - try - { - _loadedAssemblyData.Clear(); - - } - finally - { - _operationsLock.ExitWriteLock(); - } - } + this._dependencyResolvers.Clear(); + this._loadedAssemblyData.Clear(); } private readonly record struct AssemblyData @@ -364,6 +482,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService public readonly OneOf AssemblyImageOrPath; public readonly MetadataReference AssemblyReference; public readonly ImmutableArray Types; + public readonly ImmutableDictionary TypesByName; public AssemblyData(Assembly assembly, byte[] assemblyImage) { @@ -371,6 +490,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService AssemblyImageOrPath = assemblyImage ?? throw new ArgumentNullException(nameof(assemblyImage)); AssemblyReference = MetadataReference.CreateFromImage(assemblyImage); Types = assembly.GetSafeTypes().ToImmutableArray(); + TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type); } public AssemblyData(Assembly assembly, string path) @@ -379,6 +499,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService AssemblyImageOrPath = path ?? throw new ArgumentNullException(nameof(path)); AssemblyReference = MetadataReference.CreateFromFile(path); Types = assembly.GetSafeTypes().ToImmutableArray(); + TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type); } } @@ -423,6 +544,4 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService public static implicit operator AssemblyOrStringKey(Assembly assembly) => new AssemblyOrStringKey(assembly); public static implicit operator AssemblyOrStringKey(string name) => new AssemblyOrStringKey(name); } - - #endregion } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs index 629964ff7..6f4dc1159 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs @@ -97,10 +97,25 @@ public interface IAssemblyLoaderService : IService /// public FluentResults.Result> GetTypesInAssemblies(); + /// + /// Gets the list of Types from loaded assemblies. Does not create a defensive copy and blocks loading/unloading. + /// + /// + public IEnumerable UnsafeGetTypesInAssemblies(); + + /// + /// Returns the first found type given it's fully qualified name. + /// + /// + /// + public FluentResults.Result GetTypeInAssemblies(string typeName); + /// /// List of loaded assemblies. /// public IEnumerable Assemblies { get; } + + public IEnumerable AssemblyReferences { get; } }