[Refactor-Minor]
- Refactored interface definition. - Plugin Loading System Refactor (incomplete).
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ public partial class PackageService : IStylesResourcesInfo
|
||||
public IStylesService Styles => _stylesService.Value;
|
||||
|
||||
public PackageService(
|
||||
Lazy<IModConfigParserService> configParserService,
|
||||
Lazy<IModConfigCreatorService> configParserService,
|
||||
Lazy<ILuaScriptService> luaScriptService,
|
||||
Lazy<ILocalizationService> localizationService,
|
||||
Lazy<IPluginService> pluginService,
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Barotrauma.LuaCs.Services;
|
||||
public partial class PackageService
|
||||
{
|
||||
public PackageService(
|
||||
Lazy<IModConfigParserService> configParserService,
|
||||
Lazy<IModConfigCreatorService> configParserService,
|
||||
Lazy<ILuaScriptService> luaScriptService,
|
||||
Lazy<ILocalizationService> localizationService,
|
||||
Lazy<IPluginService> pluginService,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PackageReference Include="QuikGraph" Version="2.5.0" />
|
||||
<PackageReference Include="OneOf" Version="3.0.271" />
|
||||
<PackageReference Include="FluentResults" Version="3.16.0" />
|
||||
<PackageReference Include="Basic.Reference.Assemblies" Version="1.7.9" />
|
||||
<PackageReference Include="Basic.Reference.Assemblies.Net60" Version="1.7.9" />
|
||||
<PackageReference Include="ImpromptuInterface " Version="8.0.4" />
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Libraries\moonsharp\MoonSharp.Interpreter\MoonSharp.Interpreter.csproj" />
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Libraries\moonsharp\MoonSharp.VsCodeDebugger\MoonSharp.VsCodeDebugger.csproj" />
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Serves as a compound-key to refer to all resources and information that comes from a specific source.
|
||||
/// </summary>
|
||||
public interface IDataInfo
|
||||
public interface IDataInfo : IEqualityComparer<IDataInfo>, IEquatable<IDataInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
string InternalName { get; }
|
||||
/// <summary>
|
||||
@@ -17,4 +20,33 @@ public interface IDataInfo
|
||||
/// Used in place of the package data when the OwnerPackage is missing.
|
||||
/// </summary>
|
||||
string FallbackPackageName { get; }
|
||||
|
||||
bool IEqualityComparer<IDataInfo>.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<IDataInfo>.Equals(IDataInfo other)
|
||||
{
|
||||
return Equals(this, other);
|
||||
}
|
||||
|
||||
int IEqualityComparer<IDataInfo>.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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin.
|
||||
/// All plugins are loaded into their own AssemblyLoadContext along with their dependencies.
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public class AssemblyManager : IAssemblyManagementService, IPluginManagementService
|
||||
{
|
||||
#region ExternalAPI
|
||||
|
||||
public event Action<Assembly> OnAssemblyLoaded;
|
||||
public event Action<Assembly> OnAssemblyUnloading;
|
||||
public event Action<string, Exception> OnException;
|
||||
public event Action<Guid> OnACLUnload;
|
||||
public ImmutableList<WeakReference<MemoryFileAssemblyContextLoader>> 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<Type> GetSubTypesInLoadedAssemblies<T>(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<Type>.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
public bool TryGetSubTypesFromACL<T>(Guid id, out IEnumerable<Type> 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<Type> types)
|
||||
{
|
||||
if (TryGetACL(id, out var acl))
|
||||
{
|
||||
types = acl.AssembliesTypes.Select(kvp => kvp.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
types = null;
|
||||
return false;
|
||||
}
|
||||
public IEnumerable<Type> GetTypesByName(string typeName)
|
||||
{
|
||||
List<Type> 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<Guid,LoadedACL> 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<Type> 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<Type>.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
public IEnumerable<LoadedACL> GetAllLoadedACLs()
|
||||
{
|
||||
OpsLockLoaded.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (!LoadedACLs.Any())
|
||||
{
|
||||
return ImmutableList<LoadedACL>.Empty;
|
||||
}
|
||||
|
||||
return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ImmutableList<LoadedACL>.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAssemblyLoadedGlobal(string friendlyName)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InternalAPI
|
||||
|
||||
[MethodImpl(MethodImplOptions.Synchronized | MethodImplOptions.NoInlining)]
|
||||
ImmutableList<LoadedACL> IAssemblyManagementService.UnsafeGetAllLoadedACLs()
|
||||
{
|
||||
if (LoadedACLs.IsEmpty)
|
||||
return ImmutableList<LoadedACL>.Empty;
|
||||
return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList();
|
||||
}
|
||||
public event System.Func<LoadedACL, bool> IsReadyToUnloadACL;
|
||||
public AssemblyLoadingSuccessState LoadAssemblyFromMemory([NotNull] string compiledAssemblyName,
|
||||
[NotNull] IEnumerable<SyntaxTree> syntaxTree,
|
||||
IEnumerable<MetadataReference> externalMetadataReferences,
|
||||
[NotNull] CSharpCompilationOptions compilationOptions,
|
||||
string friendlyName,
|
||||
ref Guid id,
|
||||
IEnumerable<Assembly> 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<string> 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<string> 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<Guid, LoadedACL> 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<LoadedACL, bool> { } 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<MemoryFileAssemblyContextLoader>(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<WeakReference<MemoryFileAssemblyContextLoader>> toRemove = new();
|
||||
foreach (WeakReference<MemoryFileAssemblyContextLoader> weakReference in UnloadingACLs)
|
||||
{
|
||||
if (!weakReference.TryGetTarget(out _))
|
||||
{
|
||||
toRemove.Add(weakReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove.Any())
|
||||
{
|
||||
OpsLockUnloaded.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
foreach (WeakReference<MemoryFileAssemblyContextLoader> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="friendlyName">A non-unique name for later reference. Optional.</param>
|
||||
/// <param name="acl"></param>
|
||||
/// <returns>Should only return false if an error occurs.</returns>
|
||||
[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<MemoryFileAssemblyContextLoader>(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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the list of types in the default assembly load context.
|
||||
/// </summary>
|
||||
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<string, Type> 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<string, Type>.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data
|
||||
|
||||
private readonly ConcurrentDictionary<string, ImmutableList<Type>> _subTypesLookupCache = new();
|
||||
private ImmutableDictionary<string, Type> _defaultContextTypes;
|
||||
private readonly ConcurrentDictionary<Guid, LoadedACL> LoadedACLs = new();
|
||||
private readonly List<WeakReference<MemoryFileAssemblyContextLoader>> 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<string, Type> _assembliesTypes = ImmutableDictionary<string, Type>.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<string, Type> AssembliesTypes => ref _assembliesTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Warning: For use by the Assembly Manager only! Do not call this method otherwise.
|
||||
/// </summary>
|
||||
internal void ClearACLRef()
|
||||
{
|
||||
Acl = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuild the list of types from assemblies loaded in the AsmCtxLoader.
|
||||
/// </summary>
|
||||
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<string, Type> 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<string, Type>.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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ public partial class PackageService : IPackageService
|
||||
|
||||
|
||||
// mod config / package scanners/parsers
|
||||
private readonly Lazy<IModConfigParserService> _configParserService;
|
||||
private readonly Lazy<IModConfigCreatorService> _configParserService;
|
||||
private readonly Lazy<ILuaScriptService> _luaScriptService;
|
||||
private readonly Lazy<ILocalizationService> _localizationService;
|
||||
private readonly Lazy<IPluginService> _pluginService;
|
||||
|
||||
@@ -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<Guid, IAssemblyLoaderService> _assemblyServices = new();
|
||||
private readonly ConcurrentDictionary<IAssemblyResourceInfo, Guid> _resourceData = new();
|
||||
private readonly Lazy<IEventService> _eventService;
|
||||
private readonly Func<IAssemblyLoaderService> _assemblyServiceFactory;
|
||||
private ImmutableDictionary<string, Type> _cachedTypes = null;
|
||||
private ImmutableDictionary<string, Type> DefaultTypeCache => _cachedTypes ??= AssemblyLoadContext.Default.Assemblies
|
||||
.SelectMany(ass => ass.GetSafeTypes()).ToImmutableDictionary(type => type.FullName, type => type);
|
||||
|
||||
|
||||
public bool IsResourceLoaded<T>(T resource) where T : IAssemblyResourceInfo
|
||||
{
|
||||
((IService)this).CheckDisposed();
|
||||
return _resourceData.ContainsKey(resource);
|
||||
}
|
||||
|
||||
public Result<ImmutableArray<Type>> GetImplementingTypes<T>(string namespacePrefix = null, bool includeInterfaces = false,
|
||||
bool includeAbstractTypes = false, bool includeDefaultContext = true)
|
||||
{
|
||||
((IService)this).CheckDisposed();
|
||||
var types = ImmutableArray.CreateBuilder<Type>();
|
||||
_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<ImmutableArray<IAssemblyResourceInfo>> LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resource)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Result<Assembly> 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<Assembly> GetLoadedAssembly(AssemblyName assemblyName, in Guid[] excludedContexts)
|
||||
=> GetLoadedAssembly(assemblyName.FullName, excludedContexts);
|
||||
|
||||
public ImmutableArray<MetadataReference> GetDefaultMetadataReferences() =>
|
||||
Basic.Reference.Assemblies.Net60.References.All.Select(Unsafe.As<MetadataReference>).ToImmutableArray();
|
||||
|
||||
public ImmutableArray<MetadataReference> GetAddInContextsMetadataReferences()
|
||||
{
|
||||
((IService)this).CheckDisposed();
|
||||
_operationsLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (_assemblyServices.IsEmpty)
|
||||
return ImmutableArray<MetadataReference>.Empty;
|
||||
var builder = ImmutableArray.CreateBuilder<MetadataReference>();
|
||||
foreach (var context in _assemblyServices.Values)
|
||||
builder.AddRange(context.AssemblyReferences);
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationsLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public ImmutableArray<IAssemblyLoaderService> AssemblyLoaderServices { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// TODO release managed resources here
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public FluentResults.Result Reset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Barotrauma.LuaCs.Services.Processing;
|
||||
|
||||
public interface IModConfigParserService : IReusableService
|
||||
public interface IModConfigCreatorService : IService
|
||||
{
|
||||
FluentResults.Result<IModConfigInfo> BuildConfigForPackage(ContentPackage package);
|
||||
FluentResults.Result<IModConfigInfo> BuildConfigFromManifest(string manifestPath);
|
||||
@@ -18,28 +18,24 @@ public interface IPluginManagementService : IReusableService
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="package"></param>
|
||||
/// <param name="namespacePrefix"></param>
|
||||
/// <param name="includeInterfaces"></param>
|
||||
/// <param name="includeAbstractTypes"></param>
|
||||
/// <param name="includeDefaultContext"></param>
|
||||
/// <param name="includeExplicitAssembliesOnly"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
FluentResults.Result<ImmutableArray<T>> GetTypes<T>(
|
||||
ContentPackage package = null,
|
||||
FluentResults.Result<ImmutableArray<Type>> GetImplementingTypes<T>(
|
||||
string namespacePrefix = null,
|
||||
bool includeInterfaces = false,
|
||||
bool includeAbstractTypes = false,
|
||||
bool includeDefaultContext = true,
|
||||
bool includeExplicitAssembliesOnly = false);
|
||||
|
||||
bool includeDefaultContext = true);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Tries to get the
|
||||
/// </summary>
|
||||
/// <param name="package"></param>
|
||||
/// <param name="typeName"></param>
|
||||
/// <returns></returns>
|
||||
FluentResults.Result<ImmutableArray<IAssemblyResourceInfo>> GetCachedAssembliesForPackage(ContentPackage package);
|
||||
Type GetType(string typeName);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
||||
@@ -19,7 +19,7 @@ public interface IServicesProvider
|
||||
/// <param name="lifetimeInstance"></param>
|
||||
/// <typeparam name="TSvcInterface"></typeparam>
|
||||
/// <typeparam name="TService"></typeparam>
|
||||
void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IReusableService where TService : class, IReusableService, TSvcInterface;
|
||||
void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a type as a service for a given interface that can be requested by name.
|
||||
@@ -29,7 +29,7 @@ public interface IServicesProvider
|
||||
/// <param name="lifetimeInstance"></param>
|
||||
/// <typeparam name="TSvcInterface"></typeparam>
|
||||
/// <typeparam name="TService"></typeparam>
|
||||
void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IReusableService where TService : class, IReusableService, TSvcInterface;
|
||||
void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="service"></param>
|
||||
/// <param name="lifetime"></param>
|
||||
/// <typeparam name="TSvcInterface"></typeparam>
|
||||
/// <returns></returns>
|
||||
bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IReusableService;
|
||||
bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IService;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a service for the given name and interface, returns success/failure.
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="service"></param>
|
||||
/// <param name="lifetime"></param>
|
||||
/// <typeparam name="TSvcInterface"></typeparam>
|
||||
/// <returns></returns>
|
||||
bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IReusableService;
|
||||
bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IService;
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever a new service is created/instanced.
|
||||
/// Args[0]: The interface type of the service.
|
||||
/// Args[1]: The instance of the service.
|
||||
/// </summary>
|
||||
event System.Action<Type, IReusableService> OnServiceInstanced;
|
||||
event System.Action<Type, IService> OnServiceInstanced;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -89,7 +87,7 @@ public interface IServicesProvider
|
||||
/// </summary>
|
||||
/// <typeparam name="TSvc"></typeparam>
|
||||
/// <returns></returns>
|
||||
ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IReusableService;
|
||||
ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IService;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ namespace Barotrauma.LuaCs.Services;
|
||||
|
||||
public interface IStorageService : IService
|
||||
{
|
||||
#region LocalGameData
|
||||
|
||||
// -- local game folder storage
|
||||
FluentResults.Result<XDocument> LoadLocalXml(ContentPackage package, string localFilePath);
|
||||
FluentResults.Result<byte[]> LoadLocalBinary(ContentPackage package, string localFilePath);
|
||||
FluentResults.Result<string> LoadLocalText(ContentPackage package, string localFilePath);
|
||||
@@ -22,10 +21,8 @@ public interface IStorageService : IService
|
||||
Task<FluentResults.Result> SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document);
|
||||
Task<FluentResults.Result> SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes);
|
||||
Task<FluentResults.Result> SaveLocalTextAsync(ContentPackage package, string localFilePath, string text);
|
||||
|
||||
#endregion
|
||||
|
||||
#region ContentPackageData
|
||||
// -- package directory
|
||||
// singles
|
||||
FluentResults.Result<XDocument> LoadPackageXml(ContentPackage package, string localFilePath);
|
||||
FluentResults.Result<byte[]> LoadPackageBinary(ContentPackage package, string localFilePath);
|
||||
@@ -45,9 +42,7 @@ public interface IStorageService : IService
|
||||
Task<ImmutableArray<(string, FluentResults.Result<byte[]>)>> LoadPackageBinaryFilesAsync(ContentPackage package, ImmutableArray<string> localFilePaths);
|
||||
Task<ImmutableArray<(string, FluentResults.Result<string>)>> LoadPackageTextFilesAsync(ContentPackage package, ImmutableArray<string> localFilePaths);
|
||||
|
||||
#endregion
|
||||
|
||||
#region AbsolutePaths
|
||||
// -- absolute paths
|
||||
FluentResults.Result<XDocument> TryLoadXml(string filePath, Encoding encoding = null);
|
||||
FluentResults.Result<string> TryLoadText(string filePath, Encoding encoding = null);
|
||||
FluentResults.Result<byte[]> TryLoadBinary(string filePath);
|
||||
@@ -62,5 +57,4 @@ public interface IStorageService : IService
|
||||
Task<FluentResults.Result> TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null);
|
||||
Task<FluentResults.Result> TrySaveTextAsync(string filePath, string text, Encoding encoding = null);
|
||||
Task<FluentResults.Result> TrySaveBinaryAsync(string filePath, byte[] bytes);
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This bool-int wrapper increments/decrements when set as true/false respectively and return true if the value > 0.
|
||||
/// </summary>
|
||||
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<AssemblyLoader> _onUnload;
|
||||
/// <summary>
|
||||
/// This lock is just to ensure that we do not load while disposing
|
||||
/// </summary>
|
||||
private readonly ReaderWriterLockSlim _operationsLock = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private readonly ConcurrentDictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new();
|
||||
private readonly ConcurrentDictionary<AssemblyOrStringKey, AssemblyData> _loadedAssemblyData = new();
|
||||
|
||||
private ThreadLocal<bool> _isResolving = new(static()=>false); // cyclic resolution exit
|
||||
|
||||
#region PublicAPI
|
||||
private readonly ThreadLocal<bool> _isResolving = new(static()=>false); // cyclic resolution exit
|
||||
|
||||
public AssemblyLoader(IAssemblyManagementService assemblyManagementService,
|
||||
IEventService eventService,
|
||||
Guid id, string name,
|
||||
bool isReferenceOnlyMode, Action<AssemblyLoader> 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<MetadataReference> 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<string> 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<Assembly> CompileScriptAssembly(
|
||||
@@ -96,209 +126,303 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
|
||||
ImmutableArray<MetadataReference> metadataReferences,
|
||||
CSharpCompilationOptions compilationOptions = null)
|
||||
{
|
||||
if (assemblyName.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new FluentResults.Result<Assembly>().WithError(new Error($"The name provided is null!")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, syntaxTrees));
|
||||
}
|
||||
|
||||
if (_loadedAssemblyData.ContainsKey(assemblyName))
|
||||
{
|
||||
return new FluentResults.Result<Assembly>().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<Assembly>().WithSuccess($"Compiled assembly {assemblyName} successful.").WithValue(data.Assembly);
|
||||
if (assemblyName.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new Result<Assembly>().WithError(new Error($"The name provided is null!")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, syntaxTrees));
|
||||
}
|
||||
|
||||
if (_loadedAssemblyData.ContainsKey(assemblyName))
|
||||
{
|
||||
return new Result<Assembly>().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<Assembly>().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<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
|
||||
ImmutableArray<string> additionalDependencyPaths)
|
||||
{
|
||||
if (assemblyFilePath.IsNullOrWhiteSpace())
|
||||
return new FluentResults.Result<Assembly>().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<Assembly>().WithSuccess($"Loaded assembly'{assembly.GetName()}'").WithValue(assembly);
|
||||
if (assemblyFilePath.IsNullOrWhiteSpace())
|
||||
return new Result<Assembly>().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<Assembly>().WithSuccess($"Loaded assembly'{assembly.GetName()}'").WithValue(assembly);
|
||||
}
|
||||
catch (ArgumentNullException ane)
|
||||
{
|
||||
return FluentResults.Result.Fail<Assembly>(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<Assembly>(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<Assembly>(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<Assembly>(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<Assembly>(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<Assembly>(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<Assembly>(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<Assembly>(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<Assembly>(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<Assembly>(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<Assembly>(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<Assembly>(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<Assembly> 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<Assembly>().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<Assembly>().WithSuccess(new Success($"Assembly found")).WithValue(assembly1);
|
||||
return new Result<Assembly>().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<Assembly>().WithSuccess(new Success($"Assembly found")).WithValue(assembly1);
|
||||
}
|
||||
}
|
||||
|
||||
return FluentResults.Result.Fail(new Error($"Assembly named { assemblyName } not found!"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
return new FluentResults.Result<ImmutableArray<Type>>().WithValue(_loadedAssemblyData.SelectMany(kvp=> kvp.Value.Types).ToImmutableArray());
|
||||
return new FluentResults.Result<ImmutableArray<Type>>().WithValue(_loadedAssemblyData
|
||||
.SelectMany(kvp => kvp.Value.Types).ToImmutableArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return FluentResults.Result.Fail(new ExceptionalError(e));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<Type> 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<Type> 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<Type>().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<IAssemblyLoaderService>(this);
|
||||
_eventService.PublishEvent<IEventAssemblyContextUnloading>((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<byte[], string> AssemblyImageOrPath;
|
||||
public readonly MetadataReference AssemblyReference;
|
||||
public readonly ImmutableArray<Type> Types;
|
||||
public readonly ImmutableDictionary<string, Type> 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
|
||||
}
|
||||
|
||||
@@ -97,10 +97,25 @@ public interface IAssemblyLoaderService : IService
|
||||
/// <returns></returns>
|
||||
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of <c>Type</c>s from loaded assemblies. Does not create a defensive copy and blocks loading/unloading.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<Type> UnsafeGetTypesInAssemblies();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first found type given it's fully qualified name.
|
||||
/// </summary>
|
||||
/// <param name="typeName"></param>
|
||||
/// <returns></returns>
|
||||
public FluentResults.Result<Type> GetTypeInAssemblies(string typeName);
|
||||
|
||||
/// <summary>
|
||||
/// List of loaded assemblies.
|
||||
/// </summary>
|
||||
public IEnumerable<Assembly> Assemblies { get; }
|
||||
|
||||
public IEnumerable<MetadataReference> AssemblyReferences { get; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user