[Milestone] AssemblyLoader completed.

Details:
- Assembly Mgmt Service for loading now a separate interface, not intended for normal use.
- Assembly Loader work; implemented custom dictionary key and table.
- Assembly loading work.
- EventService completed.
- Moved assembly extensions to ModUtils.cs
- Work to event service.
NetworkService work
- Added ImpromptuInterfaces package.
- Networking Service work to support NetVars
- Event Service
- Added assemblies references package for script compilation. Updated Roslyn version for compatibility.
- Package Loading work.
Swap Harmony to HarmonyX
- More refactor conversion to FluentResults.
- Updated StylesService to return Results.
- Refactor of PackageService partially complete.
- Made IService.Reset() required to return a Result.
- Moved plugin/assembly related code to their own folder (same namespace).
- Updated interfaces to reflect the use of Result<T>.
- Partial refactor, incomplete.
- Added 'FluentResults' so we can stop using cursed Exception-based flow control in loading code.
- Added 'OneOf' nuget package: https://github.com/mcintyre321/OneOf
for the implementation of the Optional<T> pattern and complex discrete return types instead of cursed enums (see current AssemblyManager.cs).
- Reapplied old branch changes.
This commit is contained in:
MapleWheels
2024-11-04 02:33:31 -05:00
committed by Maplewheels
parent 01cc1d331b
commit 6880e5e9ee
97 changed files with 4100 additions and 1512 deletions
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Barotrauma
{
[Obsolete("Make your class implement IAssemblyPlugin instead.")]
public abstract class ACsMod : IAssemblyPlugin
{
private static List<ACsMod> mods = new List<ACsMod>();
public static List<ACsMod> LoadedMods { get => mods; }
private const string MOD_STORE = "LocalMods/.modstore";
public static string GetStoreFolder<T>() where T : ACsMod
{
if (!Directory.Exists(MOD_STORE)) Directory.CreateDirectory(MOD_STORE);
var modFolder = $"{MOD_STORE}/{typeof(T)}";
if (!Directory.Exists(modFolder)) Directory.CreateDirectory(modFolder);
return modFolder;
}
public bool IsDisposed { get; private set; }
/// Mod initialization
public ACsMod()
{
IsDisposed = false;
LoadedMods.Add(this);
}
/// <summary>
/// Called as soon as plugin loading begins, use this for internal setup only.
/// </summary>
public virtual void Initialize() { }
/// <summary>
/// Called once all plugins have completed Initialization. Put cross-mod code here.
/// </summary>
public virtual void OnLoadCompleted() { }
/// <summary>
/// [NotImplemented] Called before vanilla content is loaded. Use to patch Barotrauma classes before they're
/// instantiated.
/// </summary>
public void PreInitPatching() { }
public virtual void Dispose()
{
try
{
Stop();
}
catch (Exception e)
{
LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod);
}
LoadedMods.Remove(this);
IsDisposed = true;
}
public abstract void Stop();
}
}
@@ -0,0 +1,6 @@
namespace Barotrauma;
public enum ApplicationMode
{
Client, Server
}
@@ -0,0 +1,428 @@
using System;
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.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;
[assembly: InternalsVisibleTo(IAssemblyLoaderService.InternalsAwareAssemblyName)]
namespace Barotrauma.LuaCs.Services;
public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
{
public Guid Id { get; init; }
public bool IsReferenceOnlyMode { get; init; }
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
private int _isDisposed;
//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
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 FluentResults.Result AddDependencyPaths(ImmutableArray<string> paths)
{
if (paths.Length == 0)
return FluentResults.Result.Ok();
var res = new FluentResults.Result();
foreach (var path in paths)
{
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();
}
public FluentResults.Result<Assembly> CompileScriptAssembly(
[NotNull] string assemblyName,
bool compileWithInternalAccess,
ImmutableArray<SyntaxTree> syntaxTrees,
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);
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);
}
catch (Exception ex)
{
return new FluentResults.Result().WithError(new ExceptionalError(ex));
}
}
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));
}
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));
}
}
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName)
{
if (assemblyName.IsNullOrWhiteSpace())
{
return FluentResults.Result.Fail(new Error($"Assembly name is null")
.WithMetadata(MetadataType.ExceptionObject, this));
}
if (_loadedAssemblyData.TryGetValue(assemblyName, out var data))
{
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)
{
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 FluentResults.Result.Fail(new Error($"Assembly named { assemblyName } not found!"));
}
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies()
{
try
{
return new FluentResults.Result<ImmutableArray<Type>>().WithValue(_loadedAssemblyData.SelectMany(kvp=> kvp.Value.Types).ToImmutableArray());
}
catch (Exception e)
{
return FluentResults.Result.Fail(new ExceptionalError(e));
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region Internals
protected override Assembly Load(AssemblyName assemblyName)
{
if (_isResolving.Value)
return null;
_isResolving.Value = true;
try
{
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)
return ret.Value;
return null;
}
catch (ArgumentNullException _)
{
return null;
}
finally
{
_isResolving.Value = false;
}
}
// Use the default import resolver since native libraries are niche and not blocking for unloading.
// Implement if conflicts become an issue.
/*protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
// Implement NativeLibrary::InternalLoadUnmanagedDll()
throw new NotImplementedException();
}*/
private void OnUnload(AssemblyLoadContext context)
{
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();
}
}
}
private readonly record struct AssemblyData
{
public readonly Assembly Assembly;
public readonly OneOf<byte[], string> AssemblyImageOrPath;
public readonly MetadataReference AssemblyReference;
public readonly ImmutableArray<Type> Types;
public AssemblyData(Assembly assembly, byte[] assemblyImage)
{
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
AssemblyImageOrPath = assemblyImage ?? throw new ArgumentNullException(nameof(assemblyImage));
AssemblyReference = MetadataReference.CreateFromImage(assemblyImage);
Types = assembly.GetSafeTypes().ToImmutableArray();
}
public AssemblyData(Assembly assembly, string path)
{
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
AssemblyImageOrPath = path ?? throw new ArgumentNullException(nameof(path));
AssemblyReference = MetadataReference.CreateFromFile(path);
Types = assembly.GetSafeTypes().ToImmutableArray();
}
}
private readonly record struct AssemblyOrStringKey : IEquatable<AssemblyOrStringKey>, IEqualityComparer<AssemblyOrStringKey>
{
public Assembly Assembly { get; init; }
public string AssemblyName { get; init; }
public readonly int HashCode;
public AssemblyOrStringKey(Assembly assembly)
{
if(assembly == null)
throw new ArgumentNullException(nameof(assembly));
Assembly = assembly;
AssemblyName = assembly.GetName().FullName;
if (AssemblyName == null)
throw new ArgumentNullException(nameof(AssemblyName));
HashCode = AssemblyName.GetHashCode();
}
public AssemblyOrStringKey(string assemblyName)
{
if (assemblyName.IsNullOrWhiteSpace())
throw new ArgumentNullException(nameof(assemblyName));
Assembly = null;
AssemblyName = assemblyName;
HashCode = AssemblyName.GetHashCode();
}
public bool Equals(AssemblyOrStringKey x, AssemblyOrStringKey y)
{
if (x.Assembly is not null && y.Assembly is not null)
return x.Assembly == y.Assembly;
return x.AssemblyName == y.AssemblyName;
}
public int GetHashCode(AssemblyOrStringKey obj)
{
return obj.HashCode;
}
public static implicit operator AssemblyOrStringKey(Assembly assembly) => new AssemblyOrStringKey(assembly);
public static implicit operator AssemblyOrStringKey(string name) => new AssemblyOrStringKey(name);
}
#endregion
}
@@ -0,0 +1,15 @@
namespace Barotrauma;
public enum AssemblyLoadingSuccessState
{
ACLLoadFailure,
AlreadyLoaded,
BadFilePath,
CannotLoadFile,
InvalidAssembly,
NoAssemblyFound,
PluginInstanceFailure,
BadName,
CannotLoadFromStream,
Success
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using Barotrauma.LuaCs.Services;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Barotrauma.LuaCs;
public interface IAssemblyLoaderService : IService
{
/// <summary>
/// Assembly loader factory for DI registration.
/// </summary>
/// <param name="assemblyManagementService">The assembly hosting management service.</param>
/// <param name="eventService">The event service for publishing.</param>
/// <param name="id">The referencing ID. Intended to be used to distinguish between instances.</param>
/// <param name="name">The name of the friendly name instance, used for error messages.</param>
/// <param name="isReferenceOnlyMode">Loaded assemblies are not intended for execution, just MetadataReferences.</param>
delegate IAssemblyLoaderService AssemblyLoaderDelegate(
IAssemblyManagementService assemblyManagementService,
IEventService eventService, Guid id, string name,
bool isReferenceOnlyMode, Action<AssemblyLoader> onUnload);
/// <summary>
/// ID for this instance.
/// </summary>
Guid Id { get; }
/// <summary>
/// Indicates that the assemblies in this load context are metadata references only and not
/// intended for execution.
/// </summary>
bool IsReferenceOnlyMode { get; }
/// <summary>
/// Runtime value of constant <see cref="InternalsAwareAssemblyName"/> for extensibility use.
/// </summary>
public static readonly string InternalsAccessAssemblyName = InternalsAwareAssemblyName;
/// <summary>
/// Name for all runtime-compiled assemblies requiring access to <c>internal</c> assembly components. <seealso cref="InternalsVisibleToAttribute"/>
/// </summary>
public const string InternalsAwareAssemblyName = "InternalsAwareAssembly";
/// <summary>
/// Add additional locations for dependency resolution to use.
/// </summary>
/// <param name="paths"></param>
/// <returns></returns>
public FluentResults.Result AddDependencyPaths(ImmutableArray<string> paths);
/// <summary>
/// Compiles the supplied syntaxtrees and options into an in-memory assembly image.
/// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the
/// AssemblyManager class.
/// </summary>
/// <param name="assemblyName"><c>[NotNull]</c>Name reference of the assembly.
/// <para><b>[IMPORTANT]</b> This is used to reference this assembly as the true name will be forced if
/// publicized assemblies are not used (InternalsVisibleTo Attrib).</para>
/// Must be supplied for in-memory assemblies.
/// <para>Must be unique to all other assemblies explicitly loaded using this context.</para></param>
/// <param name="compileWithInternalAccess">Forces the assembly name to <see cref="InternalsAccessAssemblyName"/> and grants access to <c>internal</c>.</param>
/// <para><b>[IMPORTANT]</b>Cannot be null or empty if <see cref="compileWithInternalAccess"/> is false.</para></param>
/// <param name="syntaxTrees"><c>[NotNull]</c>Syntax trees to compile into the assembly.</param>
/// <param name="metadataReferences">All <c>MetadataReference<c/>s to be used for compilation.
/// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory
/// images not managed by the AssemblyManager class.</param>
/// <param name="compilationOptions"><c>[NotNull]</c>CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation.</param>
/// <returns>Success state of the operation.</returns>
public FluentResults.Result<Assembly> CompileScriptAssembly(
[NotNull] string assemblyName,
bool compileWithInternalAccess,
ImmutableArray<SyntaxTree> syntaxTrees,
ImmutableArray<MetadataReference> metadataReferences,
CSharpCompilationOptions compilationOptions = null);
/// <summary>
/// Loads the assembly from the provided location and registers all new paths provided with dependency resolution.
/// </summary>
/// <param name="assemblyFilePath">Absolute path to the managed assembly.</param>
/// <param name="additionalDependencyPaths">Additional paths for dependency resolution.</param>
/// <returns>Success and reference to the assembly if successful.</returns>
public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
ImmutableArray<string> additionalDependencyPaths);
/// <summary>
/// Returns the already loaded assembly with the same name.
/// </summary>
/// <param name="assemblyName">Name of the assembly.</param>
/// <returns>Operation success on assembly found and assembly.</returns>
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName);
/// <summary>
/// Gets the list of <c>Type</c>s from loaded assemblies.
/// </summary>
/// <returns></returns>
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies();
/// <summary>
/// List of loaded assemblies.
/// </summary>
public IEnumerable<Assembly> Assemblies { get; }
}
@@ -0,0 +1,6 @@
using System;
using Barotrauma.LuaCs.Events;
namespace Barotrauma;
public interface IAssemblyPlugin : IDisposable, IEventPluginPreInitialize, IEventPluginInitialize, IEventPluginLoadCompleted { }
@@ -0,0 +1,341 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using Barotrauma.LuaCs.Services;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
// ReSharper disable ConditionIsAlwaysTrueOrFalse
[assembly: InternalsVisibleTo("CompiledAssembly")]
namespace Barotrauma.LuaCs.Services;
/// <summary>
/// AssemblyLoadContext to compile from syntax trees in memory and to load from disk/file. Provides dependency resolution.
/// [IMPORTANT] Only supports 1 in-memory compiled assembly at a time. Use more instances if you need more.
/// [IMPORTANT] All file assemblies required for the compilation of syntax trees should be loaded first.
/// </summary>
public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
{
// public
public string FriendlyName { get; set; }
// ReSharper disable MemberCanBePrivate.Global
public Assembly CompiledAssembly { get; private set; }
public byte[] CompiledAssemblyImage { get; private set; }
// ReSharper restore MemberCanBePrivate.Global
// internal
private readonly Dictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new(); // path-folder, resolver
protected bool IsResolving; //this is to avoid circular dependency lookup.
private IAssemblyManagementService _assemblyManager;
public bool IsTemplateMode { get; set; }
public bool IsDisposed { get; private set; }
public MemoryFileAssemblyContextLoader(IAssemblyManagementService assemblyManager) : base(isCollectible: true)
{
this._assemblyManager = assemblyManager;
this.IsDisposed = false;
base.Unloading += OnUnload;
}
/// <summary>
/// Try to load the list of disk-file assemblies.
/// </summary>
/// <param name="assemblyFilePaths">Operation success or failure reason.</param>
public AssemblyLoadingSuccessState LoadFromFiles([NotNull] IEnumerable<string> assemblyFilePaths)
{
if (assemblyFilePaths is null)
throw new ArgumentNullException(
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(LoadFromFiles)}() | The supplied filepath list is null.");
foreach (string filepath in assemblyFilePaths)
{
// path verification
if (filepath.IsNullOrWhiteSpace())
continue;
string sanitizedFilePath = System.IO.Path.GetFullPath(filepath.CleanUpPath());
string directoryKey = System.IO.Path.GetDirectoryName(sanitizedFilePath);
if (directoryKey is null)
return AssemblyLoadingSuccessState.BadFilePath;
// setup dep resolver if not available
if (!_dependencyResolvers.ContainsKey(directoryKey) || _dependencyResolvers[directoryKey] is null)
{
_dependencyResolvers[directoryKey] = new AssemblyDependencyResolver(sanitizedFilePath); // supply the first assembly to be loaded
}
// try loading the assemblies
try
{
LoadFromAssemblyPath(sanitizedFilePath);
}
// on fail of any we're done because we assume that loaded files are related. This ACL needs to be unloaded and collected.
catch (ArgumentNullException ane)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ane.Message} | {ane.StackTrace}");
return AssemblyLoadingSuccessState.BadFilePath;
}
catch (ArgumentException ae)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ae.Message} | {ae.StackTrace}");
return AssemblyLoadingSuccessState.BadFilePath;
}
catch (FileLoadException fle)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fle.Message} | {fle.StackTrace}");
return AssemblyLoadingSuccessState.CannotLoadFile;
}
catch (FileNotFoundException fnfe)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fnfe.Message} | {fnfe.StackTrace}");
return AssemblyLoadingSuccessState.NoAssemblyFound;
}
catch (BadImageFormatException bife)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {bife.Message} | {bife.StackTrace}");
return AssemblyLoadingSuccessState.InvalidAssembly;
}
catch (Exception e)
{
#if SERVER
LuaCsLogger.LogError($"Unable to load dependency assembly file at {filepath.CleanUpPath()} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
#elif CLIENT
LuaCsLogger.ShowErrorOverlay($"Unable to load dependency assembly file at {filepath} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
#endif
return AssemblyLoadingSuccessState.ACLLoadFailure;
}
}
return AssemblyLoadingSuccessState.Success;
}
/// <summary>
/// Compiles the supplied syntaxtrees and options into an in-memory assembly image.
/// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the
/// AssemblyManager class.
/// </summary>
/// <param name="assemblyName">Name of the assembly. Must be supplied for in-memory assemblies.</param>
/// <param name="syntaxTrees">Syntax trees to compile into the assembly.</param>
/// <param name="externMetadataReferences">Metadata to be used for compilation.
/// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory
/// images not managed by the AssemblyManager class.</param>
/// <param name="compilationOptions">CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation.</param>
/// <param name="compilationMessages">Will contain any diagnostic messages for compilation failure.</param>
/// <param name="externFileAssemblyReferences">Additional assemblies located in the FileSystem to build metadata references from.
/// Assemblies here will have duplicates by the same name that are currently loaded filtered out.</param>
/// <returns>Success state of the operation.</returns>
/// <exception cref="ArgumentNullException">Throws exception if any of the required arguments are null.</exception>
public AssemblyLoadingSuccessState CompileAndLoadScriptAssembly(
[NotNull] string assemblyName,
[NotNull] IEnumerable<SyntaxTree> syntaxTrees,
IEnumerable<MetadataReference> externMetadataReferences,
[NotNull] CSharpCompilationOptions compilationOptions,
out string compilationMessages,
IEnumerable<Assembly> externFileAssemblyReferences = null)
{
compilationMessages = "";
if (this.CompiledAssembly is not null)
{
return AssemblyLoadingSuccessState.AlreadyLoaded;
}
var externAssemblyRefs = externFileAssemblyReferences is not null ? externFileAssemblyReferences.ToImmutableList() : ImmutableList<Assembly>.Empty;
var externAssemblyNames = externAssemblyRefs.Any() ? externAssemblyRefs
.Where(a => a.FullName is not null)
.Select(a => a.FullName).ToImmutableHashSet()
: ImmutableHashSet<string>.Empty;
// verifications
if (assemblyName.IsNullOrWhiteSpace())
throw new ArgumentNullException(
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied assembly name is null!");
if (syntaxTrees is null)
throw new ArgumentNullException(
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied syntax tree is null!");
// add external references
List<MetadataReference> metadataReferences = new();
if (externMetadataReferences is not null)
metadataReferences.AddRange(externMetadataReferences);
// build metadata refs from default where not an in-memory compiled assembly and not the same assembly as supplied.
metadataReferences.AddRange(AssemblyLoadContext.Default.Assemblies
.Where(a =>
{
if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
return false;
if (a.FullName is null)
return true;
return !externAssemblyNames.Contains(a.FullName); // exclude duplicates
})
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
.Union(externAssemblyRefs // add custom supplied assemblies
.Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
).ToList());
ImmutableList<AssemblyManager.LoadedACL> loadedAcls = _assemblyManager.GetAllLoadedACLs().ToImmutableList();
if (loadedAcls.Any())
{
// build metadata refs from ACL assemblies from files/disk.
foreach (AssemblyManager.LoadedACL loadedAcl in loadedAcls)
{
if(loadedAcl?.Acl is null || loadedAcl.Acl.IsTemplateMode || loadedAcl.Acl.IsDisposed)
continue;
metadataReferences.AddRange(loadedAcl.Acl.Assemblies
.Where(a =>
{
if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
return false;
if (a.FullName is null)
return true;
return !externAssemblyNames.Contains(a.FullName); // exclude duplicates
})
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
.Union(externAssemblyRefs // add custom supplied assemblies
.Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
).ToList());
}
// build metadata refs from in-memory images
foreach (var loadedAcl in loadedAcls)
{
if (loadedAcl?.Acl?.CompiledAssemblyImage is null || loadedAcl.Acl.CompiledAssemblyImage.Length == 0)
continue;
metadataReferences.Add(MetadataReference.CreateFromImage(loadedAcl.Acl.CompiledAssemblyImage));
}
}
// Change inaccessible options to allow public access to restricted members
var topLevelBinderFlagsProperty = typeof(CSharpCompilationOptions).GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
topLevelBinderFlagsProperty?.SetValue(compilationOptions, (uint)1 << 22);
// begin compilation
using var memoryCompilation = new MemoryStream();
// compile, emit
var result = CSharpCompilation.Create(assemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(memoryCompilation);
// check for errors
if (!result.Success)
{
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error);
foreach (Diagnostic diagnostic in failures)
{
compilationMessages += $"\n{diagnostic}";
}
return AssemblyLoadingSuccessState.InvalidAssembly;
}
// read compiled assembly from memory stream into an in-memory assembly & image
memoryCompilation.Seek(0, SeekOrigin.Begin); // reset
try
{
CompiledAssembly = LoadFromStream(memoryCompilation);
CompiledAssemblyImage = memoryCompilation.ToArray();
}
catch (Exception e)
{
#if SERVER
LuaCsLogger.LogError($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
#elif CLIENT
LuaCsLogger.ShowErrorOverlay($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
#endif
return AssemblyLoadingSuccessState.CannotLoadFromStream;
}
return AssemblyLoadingSuccessState.Success;
}
[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")]
protected override Assembly Load(AssemblyName assemblyName)
{
if (IsResolving)
return null; //circular resolution fast exit.
try
{
IsResolving = true;
// resolve self collection
Assembly ass = this.Assemblies.FirstOrDefault(a =>
a.FullName is not null && a.FullName.Equals(assemblyName.FullName), null);
if (ass is not null)
return ass;
// resolve to local folders
foreach (KeyValuePair<string,AssemblyDependencyResolver> pair in _dependencyResolvers)
{
var asspath = pair.Value.ResolveAssemblyToPath(assemblyName);
if (asspath is null)
continue;
ass = LoadFromAssemblyPath(asspath);
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (ass is not null)
return ass;
}
//try resolve against other loaded alcs
ImmutableList<AssemblyManager.LoadedACL> list;
try
{
list = _assemblyManager.UnsafeGetAllLoadedACLs();
}
catch
{
list = ImmutableList<AssemblyManager.LoadedACL>.Empty;
}
if (!list.IsEmpty)
{
foreach (var loadedAcL in list)
{
if (loadedAcL.Acl is null || loadedAcL.Acl.IsTemplateMode || loadedAcL.Acl.IsDisposed)
continue;
try
{
ass = loadedAcL.Acl.LoadFromAssemblyName(assemblyName);
if (ass is not null)
return ass;
}
catch
{
// LoadFromAssemblyName throws, no need to propagate
}
}
}
ass = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
if (ass is not null)
return ass;
}
finally
{
IsResolving = false;
}
return null;
}
private void OnUnload(AssemblyLoadContext alc)
{
CompiledAssembly = null;
CompiledAssemblyImage = null;
_dependencyResolvers.Clear();
_assemblyManager = null;
base.Unloading -= OnUnload;
this.IsDisposed = true;
}
}
@@ -0,0 +1,131 @@
using System;
using System.ComponentModel;
using System.Xml.Serialization;
using Barotrauma.LuaCs.Data;
namespace Barotrauma;
[Serializable]
public sealed class RunConfig : IRunConfig
{
/// <summary>
/// How should scripts be run on the server.
/// </summary>
[XmlElement(ElementName = "Server")]
[DefaultValue("Standard")]
public string Server { get; set; }
/// <summary>
/// How should scripts be run on the client.
/// </summary>
[XmlElement(ElementName = "Client")]
[DefaultValue("Standard")]
public string Client { get; set; }
/// <summary>
/// List of dependencies by either Steam Workshop ID or by Partial Inclusive Name (ie. "ModDep" will match a mod named "A ModDependency").
/// PIN Dependency checks if ContentPackage names contains the dependency string.
/// </summary>
[XmlArrayItem(ElementName = "Dependency", IsNullable = true, Type = typeof(Dependency))]
[XmlArray]
public Dependency[] Dependencies { get; set; }
/// <summary>
/// Compiles the mod using non-publicized assemblies.
/// </summary>
[XmlElement(ElementName = "UseNonPublicizedAssemblies")]
public bool UseNonPublicizedAssemblies { get; set; }
/// <summary>
/// If the mod includes source files, the compiled assembly will be named "CompiledAssembly" and have the [InternalVisibleTo()] attribute applied to it.
/// </summary>
[XmlElement(ElementName = "UseInternalAssemblyName")]
[DefaultValue(false)]
public bool UseInternalAssemblyName { get; set; }
[XmlElement(ElementName = "AutoGenerated")]
public bool AutoGenerated { get; set; }
public RunConfig(bool autoGenerated)
{
this.AutoGenerated = autoGenerated;
if (autoGenerated)
{
(Client, Server) = ("Standard", "Standard");
UseNonPublicizedAssemblies = false;
}
}
public RunConfig() { } // For serialization use
[Serializable]
public sealed class Dependency
{
/// <summary>
/// Steam Workshop ID of the dependency.
/// </summary>
[XmlElement(ElementName = "SteamWorkshopId")]
public ulong SteamWorkshopId;
/// <summary>
/// Package Name of the dependency. Not needed if SteamWorkshopId is set.
/// </summary>
[XmlElement(ElementName = "PackageName")]
public string PackageName;
}
public RunConfig Sanitize()
{
try
{
Client = SanitizeRunSetting(Client);
}
catch (Exception)
{
Client = "Standard";
}
try
{
Server = SanitizeRunSetting(Server);
}
catch (Exception)
{
Server = "Standard";
}
Dependencies ??= new RunConfig.Dependency[] { };
static string SanitizeRunSetting(string str) =>
str switch
{
null => "Standard",
"" => "Standard",
" " => "Standard",
_ => str[0].ToString().ToUpper() + str.Substring(1).ToLower()
};
return this;
}
public bool IsForced()
{
#if CLIENT
return this.Client == "Forced";
#elif SERVER
return this.Server == "Forced";
#endif
}
public bool IsStandard()
{
#if CLIENT
return this.Client == "Standard";
#elif SERVER
return this.Server == "Standard";
#endif
}
public bool IsForcedOrStandard() => this.IsForced() || this.IsStandard();
}