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:
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>()
|
||||
|
||||
Reference in New Issue
Block a user