Scriptloading overhaul (#163)

* - Added option to RunConfig.cs for mods to compile with non-publicized assemblies.
- Added default values attrib to RunConfig.cs
- Added thread sleeping to CsPackageManager.Dispose() to give time before generating a strong-ref to check acl unload status.

* - Added PrintWarning to ModUtils.cs
- Made previously unloaded ACLs warning only.
- Made plugins unload after lua script disposal.
- Small code cleanups.
- Cleared acl record references on unloading.
- Made ACL unload run via event hook.

* Made non-pub asm compilation name the same long-a** name in XML, as per Evil's request.
This commit is contained in:
MapleWheels
2023-10-24 17:51:48 -04:00
committed by GitHub
parent e984633ca5
commit 9e48dda739
7 changed files with 80 additions and 32 deletions

View File

@@ -281,8 +281,6 @@ namespace Barotrauma
{
UserData.UnregisterType(type, true);
}
PluginPackageManager.UnloadPlugins(); // stop plugin code execution
if (Lua?.Globals is not null)
{
@@ -309,6 +307,9 @@ namespace Barotrauma
// we can only unload assemblies after clearing ModStore/references.
PluginPackageManager.Dispose();
#pragma warning disable CS0618
ACsMod.LoadedMods.Clear();
#pragma warning restore CS0618
Game = new LuaGame();
Networking = new LuaCsNetworking();

View File

@@ -9,6 +9,7 @@ using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.CodeAnalysis;
using Microsoft.Xna.Framework;
namespace Barotrauma;
@@ -26,6 +27,15 @@ public static class ModUtils
LuaCsLogger.LogMessage($"[Client] {s}");
#endif
}
public static void PrintWarning(string s)
{
#if SERVER
LuaCsLogger.Log($"[Server] {s}", Color.Yellow);
#else
LuaCsLogger.Log($"[Client] {s}", Color.Yellow);
#endif
}
public static void PrintError(string s)
{

View File

@@ -481,6 +481,7 @@ public class AssemblyManager
try
{
_subTypesLookupCache.Clear();
_defaultContextTypes = _defaultContextTypes.Clear();
foreach (KeyValuePair<Guid, LoadedACL> loadedAcl in LoadedACLs)
{
@@ -506,6 +507,7 @@ public class AssemblyManager
UnloadingACLs.Add(new WeakReference<MemoryFileAssemblyContextLoader>(loadedAcl.Value.Acl, true));
loadedAcl.Value.ClearTypesList();
loadedAcl.Value.Acl.Unload();
loadedAcl.Value.ClearACLRef();
OnACLUnload?.Invoke(loadedAcl.Value.Id);
}
}
@@ -534,6 +536,8 @@ public class AssemblyManager
OpsLockUnloaded.EnterUpgradeableReadLock();
try
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); // force the gc to collect unloaded acls.
GC.WaitForPendingFinalizers();
List<WeakReference<MemoryFileAssemblyContextLoader>> toRemove = new();
foreach (WeakReference<MemoryFileAssemblyContextLoader> weakReference in UnloadingACLs)
{
@@ -666,6 +670,7 @@ public class AssemblyManager
_subTypesLookupCache.Clear();
UnloadingACLs.Add(new WeakReference<MemoryFileAssemblyContextLoader>(acl.Acl, true));
acl.Acl.Unload();
acl.ClearACLRef();
OnACLUnload?.Invoke(acl.Id);
return true;
@@ -740,8 +745,8 @@ public class AssemblyManager
private ImmutableDictionary<string, Type> _defaultContextTypes;
private readonly ConcurrentDictionary<Guid, LoadedACL> LoadedACLs = new();
private readonly List<WeakReference<MemoryFileAssemblyContextLoader>> UnloadingACLs= new();
private readonly ReaderWriterLockSlim OpsLockLoaded = new ReaderWriterLockSlim();
private readonly ReaderWriterLockSlim OpsLockUnloaded = new ReaderWriterLockSlim();
private readonly ReaderWriterLockSlim OpsLockLoaded = new ();
private readonly ReaderWriterLockSlim OpsLockUnloaded = new ();
#endregion
@@ -752,7 +757,7 @@ public class AssemblyManager
{
public readonly Guid Id;
private ImmutableDictionary<string, Type> _assembliesTypes = ImmutableDictionary<string, Type>.Empty;
public readonly MemoryFileAssemblyContextLoader Acl;
public MemoryFileAssemblyContextLoader Acl { get; private set; }
internal LoadedACL(Guid id, AssemblyManager manager, string friendlyName)
{
@@ -763,6 +768,14 @@ public class AssemblyManager
};
}
public ImmutableDictionary<string, Type> AssembliesTypes => _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.

View File

@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using Barotrauma.Steam;
@@ -73,7 +74,7 @@ public sealed class CsPackageManager : IDisposable
private const string SCRIPT_FILE_REGEX = "*.cs";
private const string ASSEMBLY_FILE_REGEX = "*.dll";
private readonly float _assemblyUnloadTimeoutSeconds = 4f;
private readonly float _assemblyUnloadTimeoutSeconds = 6f;
private Guid _publicizedAssemblyLoader;
private readonly List<ContentPackage> _currentPackagesByLoadOrder = new();
private readonly Dictionary<ContentPackage, ImmutableList<ContentPackage>> _packagesDependencies = new();
@@ -203,10 +204,19 @@ public sealed class CsPackageManager : IDisposable
/// </summary>
public event Action OnDispose;
[MethodImpl(MethodImplOptions.Synchronized)]
public void Dispose()
{
// send events for cleanup
OnDispose?.Invoke();
try
{
OnDispose?.Invoke();
}
catch (Exception e)
{
ModUtils.Logging.PrintError($"Error while executing Dispose event: {e.Message}");
}
// cleanup events
if (OnDispose is not null)
{
@@ -219,10 +229,15 @@ public sealed class CsPackageManager : IDisposable
// cleanup plugins and assemblies
ReflectionUtils.ResetCache();
UnloadPlugins();
// try cleaning up the assemblies
_pluginTypes.Clear(); // remove assembly references
_loadedPlugins.Clear();
_publicizedAssemblyLoader = Guid.Empty;
_packagesDependencies.Clear();
_loadedCompiledPackageAssemblies.Clear();
_reverseLookupGuidList.Clear();
_packageRunConfigs.Clear();
_currentPackagesByLoadOrder.Clear();
// lua cleanup
foreach (var kvp in _luaRegisteredTypes)
@@ -240,6 +255,7 @@ public sealed class CsPackageManager : IDisposable
// we can't wait forever or app dies but we can try to be graceful
while (!_assemblyManager.TryBeginDispose())
{
Thread.Sleep(20); // give the assembly context unloader time to run (async)
if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now)
{
break;
@@ -247,8 +263,10 @@ public sealed class CsPackageManager : IDisposable
}
_assemblyUnloadStartTime = DateTime.Now;
Thread.Sleep(100); // give the garbage collector time to finalize the disposed assemblies.
while (!_assemblyManager.FinalizeDispose())
{
Thread.Sleep(100); // give the garbage collector time to finalize the disposed assemblies.
if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now)
{
break;
@@ -258,17 +276,8 @@ public sealed class CsPackageManager : IDisposable
_assemblyManager.OnAssemblyLoaded -= AssemblyManagerOnAssemblyLoaded;
_assemblyManager.OnAssemblyUnloading -= AssemblyManagerOnAssemblyUnloading;
_publicizedAssemblyLoader = Guid.Empty;
// clear lists after cleaning up
_packagesDependencies.Clear();
_loadedCompiledPackageAssemblies.Clear();
_reverseLookupGuidList.Clear();
_packageRunConfigs.Clear();
_currentPackagesByLoadOrder.Clear();
AssembliesLoaded = false;
GC.SuppressFinalize(this);
GC.SuppressFinalize(this);
}
/// <summary>
@@ -288,16 +297,16 @@ public sealed class CsPackageManager : IDisposable
// log error if some ACLs are still unloading (some assembly is still in use)
if (_assemblyManager.IsCurrentlyUnloading)
{
ModUtils.Logging.PrintError($"WARNING: Some mods from a previous session (lobby) are still loaded! This may result in undefined behaviour! Please restart your game. \nIf you wish to avoid this issue in the future, please disable the below mods and report the error to the mod author.");
ModUtils.Logging.PrintWarning($"WARNING: Some mods from a previous session (lobby) are still loaded! This may result in undefined behaviour!\nIf you notice any odd behaviour that only occurs after multiple lobbies, please restart your game.");
foreach (var wkref in _assemblyManager.StillUnloadingACLs)
{
ModUtils.Logging.PrintError($"The below ACL is still unloading:");
ModUtils.Logging.PrintWarning($"The below ACL is still unloading:");
if (wkref.TryGetTarget(out var tgt))
{
ModUtils.Logging.PrintError($"ACL Name: {tgt.Name}");
ModUtils.Logging.PrintWarning($"ACL Name: {tgt.Name}");
foreach (Assembly assembly in tgt.Assemblies)
{
ModUtils.Logging.PrintError($"-- Assembly: {assembly.GetName()}");
ModUtils.Logging.PrintWarning($"-- Assembly: {assembly.GetName()}");
}
}
}
@@ -480,7 +489,8 @@ public sealed class CsPackageManager : IDisposable
cp,
new LoadableData(
TryScanPackagesForAssemblies(cp, out var list1) ? list1 : null,
TryScanPackageForScripts(cp, out var list2) ? list2 : null)))
TryScanPackageForScripts(cp, out var list2) ? list2 : null,
GetRunConfigForPackage(cp))))
.ToImmutableDictionary();
HashSet<ContentPackage> badPackages = new();
@@ -568,7 +578,7 @@ public sealed class CsPackageManager : IDisposable
syntaxTrees,
null,
CompilationOptions,
pair.Key.Name, ref id, publicizedAssemblies);
pair.Key.Name, ref id, pair.Value.config.UseNonPublicizedAssemblies ? null : publicizedAssemblies);
if (successState is not AssemblyLoadingSuccessState.Success)
{
@@ -1077,7 +1087,7 @@ public sealed class CsPackageManager : IDisposable
BadPackage
}
private record LoadableData(ImmutableList<string> AssembliesFilePaths, ImmutableList<string> ScriptsFilePaths);
private record LoadableData(ImmutableList<string> AssembliesFilePaths, ImmutableList<string> ScriptsFilePaths, RunConfig config);
#endregion
}

View File

@@ -34,6 +34,7 @@ public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true)
{
this._assemblyManager = assemblyManager;
base.Unloading += OnUnload;
}
@@ -278,10 +279,11 @@ public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
}
public new void Unload()
private void OnUnload(AssemblyLoadContext alc)
{
CompiledAssembly = null;
CompiledAssemblyImage = null;
base.Unload();
_dependencyResolvers.Clear();
base.Unloading -= OnUnload;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Xml.Serialization;
namespace Barotrauma;
@@ -9,12 +10,16 @@ public sealed class RunConfig
/// <summary>
/// How should scripts be run on the server.
/// </summary>
[XmlElement(ElementName = "Server")] public string Server;
[XmlElement(ElementName = "Server")]
[DefaultValue("Standard")]
public string Server;
/// <summary>
/// How should scripts be run on the client.
/// </summary>
[XmlElement(ElementName = "Client")] public string Client;
[XmlElement(ElementName = "Client")]
[DefaultValue("Standard")]
public string Client;
/// <summary>
/// List of dependencies by either Steam Workshop ID or by Partial Inclusive Name (ie. "ModDep" will match a mod named "A ModDependency").
@@ -23,7 +28,14 @@ public sealed class RunConfig
[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")]
[DefaultValue(false)]
public bool UseNonPublicizedAssemblies { get; set; }
[XmlElement(ElementName = "AutoGenerated")]
public bool AutoGenerated { get; set; }
@@ -33,6 +45,7 @@ public sealed class RunConfig
if (autoGenerated)
{
(Client, Server) = ("Standard", "Standard");
UseNonPublicizedAssemblies = false;
}
}

View File

@@ -10,8 +10,7 @@ namespace Barotrauma
{
public static class ReflectionUtils
{
private static readonly ConcurrentDictionary<Assembly, ImmutableArray<Type>> CachedNonAbstractTypes
= new ConcurrentDictionary<Assembly, ImmutableArray<Type>>();
private static readonly ConcurrentDictionary<Assembly, ImmutableArray<Type>> CachedNonAbstractTypes = new();
private static readonly ConcurrentDictionary<string, ImmutableArray<Type>> TypeSearchCache = new();
public static IEnumerable<Type> GetDerivedNonAbstract<T>()