From cfde6f3579f521658fcc55f298abc2a1a75c0c71 Mon Sep 17 00:00:00 2001 From: MapleWheels Date: Fri, 15 May 2026 18:41:39 -0400 Subject: [PATCH] - Fixed assembly unloading. However, requires 'plugin_forcerungc' to be run multiple times over ~30 seconds at the main menu. --- .../SharedSource/LuaCs/LuaCsSetup.cs | 20 +- .../LuaCs/_Plugins/AssemblyLoader.cs | 22 ++- .../_Services/PluginManagementService.cs | 179 ++++++++++-------- 3 files changed, 123 insertions(+), 98 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index 03c2501b9..a5986d447 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -332,17 +332,9 @@ namespace Barotrauma void RunStateUnloaded_OnEnter(State currentState) { Logger.LogMessage("LuaCs unloaded state entered"); - - if (PackageManagementService.IsAnyPackageRunning()) - { - Logger.LogResults(PackageManagementService.StopRunningPackages()); - } - - if (PackageManagementService.IsAnyPackageLoaded()) - { - DisposeLuaCsConfig(); - Logger.LogResults(PackageManagementService.UnloadAllPackages()); - } + Logger.LogResults(PackageManagementService.StopRunningPackages()); + DisposeLuaCsConfig(); + Logger.LogResults(PackageManagementService.UnloadAllPackages()); EventService.Reset(); ConfigService.Reset(); @@ -362,11 +354,7 @@ namespace Barotrauma void RunStateLoadedNoExec_OnEnter(State currentState) { Logger.LogMessage("LuaCs no execution state entered"); - - if (PackageManagementService.IsAnyPackageRunning()) - { - Logger.LogResults(PackageManagementService.StopRunningPackages()); - } + Logger.LogResults(PackageManagementService.StopRunningPackages()); if (!PackageManagementService.IsAnyPackageLoaded()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs index 292bb3c35..7ff0a6f15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs @@ -256,6 +256,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService } } + [MethodImpl(MethodImplOptions.NoInlining)] public Result CompileScriptAssembly([NotNull] string assemblyName, bool compileWithInternalAccess, ImmutableArray syntaxTrees, @@ -348,6 +349,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService } } + [MethodImpl(MethodImplOptions.NoInlining)] public FluentResults.Result LoadAssemblyFromFile(string assemblyFilePath, ImmutableArray additionalDependencyPaths) { @@ -434,6 +436,8 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService } } + + [MethodImpl(MethodImplOptions.NoInlining)] public FluentResults.Result GetAssemblyByName(string assemblyName) { if (IsDisposed) @@ -481,6 +485,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService } } + [MethodImpl(MethodImplOptions.NoInlining)] public FluentResults.Result> GetTypesInAssemblies() { if (IsDisposed) @@ -501,6 +506,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService } } + [MethodImpl(MethodImplOptions.NoInlining)] public IEnumerable UnsafeGetTypesInAssemblies() { if (IsDisposed) @@ -529,6 +535,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService } } + [MethodImpl(MethodImplOptions.NoInlining)] public Result GetTypeInAssemblies(string typeName) { if (IsDisposed) @@ -557,14 +564,12 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService return; // we don't want to invoke events twice nor cause strong GC handles. IsDisposed = true; this.Unload(); - this.DisposeInternal(); - // we want to call base finalizers - //GC.SuppressFinalize(this); + GC.SuppressFinalize(this); } ~AssemblyLoader() { - this.DisposeInternal(); + this.Unload(); } private void OnUnload(AssemblyLoadContext context) @@ -579,9 +584,8 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService Thread.Sleep(1000/Timing.FixedUpdateRate-1); } - var wf = new WeakReference(this); - _loadedAssemblyData.Clear(); _onUnload?.Invoke(this); + this.DisposeInternal(); } private void DisposeInternal() @@ -592,6 +596,9 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService base.Unloading -= OnUnload; this._dependencyResolvers.Clear(); this._loadedAssemblyData.Clear(); + + GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); + GC.WaitForFullGCComplete(10); } protected override Assembly Load(AssemblyName assemblyName) @@ -660,6 +667,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService public readonly ImmutableArray Types; public readonly ImmutableDictionary TypesByName; + [MethodImpl(MethodImplOptions.NoOptimization)] public AssemblyData(Assembly assembly, byte[] assemblyImage) { Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); @@ -669,6 +677,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type); } + [MethodImpl(MethodImplOptions.NoOptimization)] public AssemblyData(Assembly assembly, string path) { Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); @@ -696,6 +705,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService HashCode = AssemblyName.GetHashCode(); } + [MethodImpl(MethodImplOptions.NoOptimization)] public AssemblyOrStringKey(string assemblyName) { if (assemblyName.IsNullOrWhiteSpace()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs index c497415b0..f9bd45982 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs @@ -89,6 +89,7 @@ public class PluginManagementService : IAssemblyManagementService private ImmutableArray _baseMetadataReferences = ImmutableArray.Empty; private ImmutableArray _baseMetadataReferencesNonPublicized = ImmutableArray.Empty; + private static readonly int GC_COLLECT_WAIT_TIME = 2000; private IEnumerable BaseMetadataReferences { @@ -215,6 +216,7 @@ public class PluginManagementService : IAssemblyManagementService private IEventService _pluginEventService; private Lazy _pluginLuaPatcherService; private Func _consoleCommandServiceFactory; + private readonly IConsoleCommandsService _internalConsoleCommandsService; private ILuaCsInfoProvider _luaCsInfoProvider; private readonly ConcurrentDictionary _assemblyLoaders = new(); private readonly ConcurrentDictionary _pluginPackageLookup = new(); @@ -244,6 +246,18 @@ public class PluginManagementService : IAssemblyManagementService _pluginLuaPatcherService = pluginLuaPatcherService; _consoleCommandServiceFactory = consoleCommandServiceFactory; _luaCsInfoProvider = luaCsInfoProvider; + _internalConsoleCommandsService = consoleCommandServiceFactory.Invoke(); + + RegisterCommands(_internalConsoleCommandsService); + } + + private void RegisterCommands(IConsoleCommandsService cmdService) + { + cmdService.RegisterCommand("plugin_forcerungc", "Forces the GC to run", cmds => + { + _logger.LogMessage("Forcing GC run."); + RunGC(true); + }); } private ServiceContainer CreatePluginServiceContainer() @@ -310,11 +324,13 @@ public class PluginManagementService : IAssemblyManagementService } } + [MethodImpl(MethodImplOptions.NoInlining)] public bool TryGetPackageForPlugin(out ContentPackage ownerPackage) { return _pluginPackageLookup.TryGetValue(typeof(TPlugin), out ownerPackage); } + [MethodImpl(MethodImplOptions.NoInlining)] public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, bool includeDefaultContext = true) { @@ -497,7 +513,7 @@ public class PluginManagementService : IAssemblyManagementService } } - + [MethodImpl(MethodImplOptions.NoInlining)] public FluentResults.Result LoadAssemblyResources(ImmutableArray resources) { if (resources.IsDefaultOrEmpty) @@ -727,7 +743,7 @@ public class PluginManagementService : IAssemblyManagementService .Replace(" Barotrauma.Networking.Client.ClientList", " ModUtils.Client.ClientList") .Replace("ItemPrefab.GetItemPrefab", "ModUtils.ItemPrefab.GetItemPrefab"); } - + private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly callerAssembly, string targetAssemblyName) { Guard.IsNull(callerAssembly, nameof(callerAssembly)); @@ -800,21 +816,58 @@ public class PluginManagementService : IAssemblyManagementService { _eventService?.Value?.PublishEvent(sub => sub.OnAssemblyUnloading(assembly)); } + + _unloadingAssemblyLoaders.Add(loader, loader.OwnerPackage); } - + + [MethodImpl(MethodImplOptions.NoOptimization)] public FluentResults.Result UnloadManagedAssemblies() { using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); - if (_assemblyLoaders.Count == 0) - { - return FluentResults.Result.Ok(); - } - var results = new FluentResults.Result(); - results.WithReasons(UnsafeDisposeManagedTypeInstances().Reasons); + if (!_pluginInstances.IsEmpty) + { + foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value)) + { + try + { + instance.Dispose(); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); + continue; + } + } + _pluginInstances.Clear(); + } + + if (_pluginEventService is not null) + { + _eventService.Value.RemoveDispatcherEventService(_pluginEventService); + try + { + _pluginEventService.Dispose(); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); + } + _pluginEventService = null; + } + + try + { + _pluginInjectorContainer?.Dispose(); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); + } + _pluginInjectorContainer = null; ReflectionUtils.ResetCache(); foreach (var loaderService in _assemblyLoaders) @@ -822,7 +875,6 @@ public class PluginManagementService : IAssemblyManagementService try { loaderService.Value.Dispose(); - _unloadingAssemblyLoaders.Add(loaderService.Value, loaderService.Key); } catch (Exception e) { @@ -832,41 +884,7 @@ public class PluginManagementService : IAssemblyManagementService _assemblyLoaders.Clear(); _storageService.PurgeCache(); - GC.Collect(); - GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true); - GC.WaitForPendingFinalizers(); - GC.WaitForFullGCComplete(1000); - -#if DEBUG - // Print still loaded assembly load ctx after giving some time - CoroutineManager.Invoke(() => - { - if (!_unloadingAssemblyLoaders.Any()) - { - return; - } - - StringBuilder sb = new StringBuilder(); - - sb.AppendLine("The following ContentPackages have not unloaded their assemblies:"); - - foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray()) - { - sb.AppendLine($"- '{kvp.Value.Name}'"); - } - - - // Use DebugConsole in case logger is null by the time this executes. - if (_logger is null) - { - DebugConsole.LogError(sb.ToString()); - } - else - { - _logger.LogWarning(sb.ToString()); - } - }, 3.0f); -#endif + _pluginPackageLookup.Clear(); // clear native libraries if (_loadedNativeLibraries.Any()) @@ -886,49 +904,58 @@ public class PluginManagementService : IAssemblyManagementService _loadedNativeLibraries.Clear(); } + + RunGC(false); return results; } - private FluentResults.Result UnsafeDisposeManagedTypeInstances() + private void RunGC(bool logResults) { - var results = new FluentResults.Result(); - - if (!_pluginInstances.IsEmpty) + int maxGen = GC.MaxGeneration; + GC.RegisterForFullGCNotification(maxGen, 10); + for (int gcGen = 0; gcGen < maxGen; gcGen++) { - foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value)) + GC.Collect(maxGen, GCCollectionMode.Aggressive, true, true); + var confirmationToken = GC.WaitForFullGCComplete(GC_COLLECT_WAIT_TIME); + if (logResults) { - try - { - instance.Dispose(); - } - catch (Exception e) - { - results.WithError(new ExceptionalError(e)); - continue; - } + _logger.LogWarning($"GC Pass # {gcGen} completed. Completion status: {confirmationToken.ToString()}"); } } + GC.CancelFullGCNotification(); - if (_pluginEventService is not null) + // Print still loaded assembly load ctx after giving some time + if (logResults) { - _eventService.Value.RemoveDispatcherEventService(_pluginEventService); - _pluginEventService = null; + CoroutineManager.Invoke(() => + { + if (!_unloadingAssemblyLoaders.Any()) + { + return; + } + + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("The following ContentPackages have not unloaded their assemblies:"); + + foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray()) + { + sb.AppendLine($"- '{kvp.Value.Name}'"); + } + + + // Use DebugConsole in case logger is null by the time this executes. + if (_logger is null) + { + DebugConsole.LogError(sb.ToString()); + } + else + { + _logger.LogWarning(sb.ToString()); + } + }, GC.MaxGeneration * GC_COLLECT_WAIT_TIME/1000f); } - try - { - _pluginInjectorContainer.Dispose(); - } - catch (Exception e) - { - results.WithError(new ExceptionalError(e)); - } - _pluginInjectorContainer = null; - - _pluginInstances.Clear(); - _pluginPackageLookup.Clear(); - - return results; } public Result GetLoadedAssembly(OneOf assemblyName, in Guid[] excludedContexts)