From 21606b24c47c2a20c163f6d52eb728d1b1c6211c Mon Sep 17 00:00:00 2001 From: Remaarn Date: Tue, 9 Sep 2025 16:28:12 +1000 Subject: [PATCH 1/4] Format PluginManager --- Torch/Plugins/PluginManager.cs | 168 +++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 71 deletions(-) diff --git a/Torch/Plugins/PluginManager.cs b/Torch/Plugins/PluginManager.cs index 560e476e..61ffcb04 100644 --- a/Torch/Plugins/PluginManager.cs +++ b/Torch/Plugins/PluginManager.cs @@ -1,23 +1,17 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; using System.IO.Compression; using System.Linq; -using System.Net; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using System.Windows; -using System.Xml.Serialization; -using Havok; using NLog; using Torch.API; using Torch.API.Managers; using Torch.API.Plugins; using Torch.API.Session; -using Torch.API.WebAPI; using Torch.Collections; using Torch.Commands; using Torch.Utils; @@ -28,9 +22,9 @@ namespace Torch.Managers /// public class PluginManager : Manager, IPluginManager { - //event for when the plugins are reloaded public event Action PluginsReloaded; + private class PluginItem { public string Filename { get; set; } @@ -39,35 +33,34 @@ private class PluginItem public bool IsZip { get; set; } public List ResolvedDependencies { get; set; } } - + private static Logger _log = LogManager.GetCurrentClassLogger(); - + private const string MANIFEST_NAME = "manifest.xml"; - + public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"); private readonly MtObservableSortedDictionary _plugins = new MtObservableSortedDictionary(); private readonly List _pluginItems = new List(); private readonly List _reloadList = new List(); private CommandManager _mgr; - + #pragma warning disable 649 [Dependency] private ITorchSessionManager _sessionManager; #pragma warning restore 649 - + /// public IReadOnlyDictionary Plugins => _plugins.AsReadOnlyObservable(); public event Action> PluginsLoaded; - + public PluginManager(ITorchBase torchInstance) : base(torchInstance) { Task.Run(async () => await TestApiConnection()).Wait(); - if (!Directory.Exists(PluginDir)) - Directory.CreateDirectory(PluginDir); + Directory.CreateDirectory(PluginDir); } - + /// /// Updates loaded plugins in parallel. /// @@ -85,7 +78,7 @@ public void UpdatePlugins() } } } - + /// public override void Attach() { @@ -96,8 +89,10 @@ public override void Attach() private void SessionManagerOnSessionStateChanged(ITorchSession session, TorchSessionState newState) { _mgr = session.Managers.GetManager(); + if (_mgr == null) return; + switch (newState) { case TorchSessionState.Loaded: @@ -114,13 +109,14 @@ private void SessionManagerOnSessionStateChanged(ITorchSession session, TorchSes return; } } - + /// /// Unloads all plugins. /// public override void Detach() { _sessionManager.SessionStateChanged -= SessionManagerOnSessionStateChanged; + foreach (var plugin in _plugins.Values) plugin.Dispose(); @@ -145,6 +141,7 @@ public void LoadPlugins() { plugin.Init(Torch); } + _log.Info($"Loaded {_plugins.Count} plugins."); PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly()); return; @@ -152,26 +149,29 @@ public void LoadPlugins() var pluginItems = GetLocalPlugins(PluginDir); var pluginsToLoad = new List(); + foreach (var item in pluginItems) { var pluginItem = item; + if (!TryValidatePluginDependencies(pluginItems, ref pluginItem, out var missingPlugins)) { // We have some missing dependencies. // Future fix would be to download them, but instead for now let's // just warn the user it's missing - foreach(var missingPlugin in missingPlugins) + foreach (var missingPlugin in missingPlugins) _log.Warn($"{item.Manifest.Name} is missing dependency {missingPlugin}. Skipping plugin."); + continue; } - + pluginsToLoad.Add(pluginItem); } - + _log.Info($"Is plugin API reachable: {IsApiReachable}"); + if (IsApiReachable) { - if (Torch.Config.ShouldUpdatePlugins) { if (DownloadPluginUpdates(pluginsToLoad)) @@ -179,14 +179,16 @@ public void LoadPlugins() // Resort the plugins just in case updates changed load hints. pluginItems = GetLocalPlugins(PluginDir); pluginsToLoad.Clear(); + foreach (var item in pluginItems) { var pluginItem = item; + if (!TryValidatePluginDependencies(pluginItems, ref pluginItem, out var missingPlugins)) { foreach (var missingPlugin in missingPlugins) - _log.Warn( - $"{item.Manifest.Name} is missing dependency {missingPlugin}. Skipping plugin."); + _log.Warn($"{item.Manifest.Name} is missing dependency {missingPlugin}. Skipping plugin."); + continue; } @@ -227,16 +229,15 @@ public void LoadPlugins() _pluginItems.Add(plugin); LoadPlugin(plugin); } - + foreach (var plugin in _plugins.Values) { plugin.Init(Torch); } } - + _reloadList.Clear(); - - + _log.Info($"Loaded {_plugins.Count} plugins."); PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly()); } @@ -246,11 +247,13 @@ public void LoadPlugins() private List GetLocalPlugins(string pluginDir, bool debug = false) { var firstLoad = Torch.Config.Plugins.Count == 0; - + var pluginItems = Directory.EnumerateFiles(pluginDir, "*.zip") .Union(Directory.EnumerateDirectories(pluginDir)); + if (debug) - pluginItems = pluginItems.Union(new List {pluginDir}); + pluginItems = pluginItems.Union(new List { pluginDir }); + var results = new List(); foreach (var item in pluginItems) @@ -266,22 +269,22 @@ private List GetLocalPlugins(string pluginDir, bool debug = false) _log.Warn($"Item '{item}' is missing a manifest, skipping."); continue; } - manifest = new PluginManifest() - { - Guid = new Guid(), - Version = "0", - Name = "TEST" - }; + + manifest = new PluginManifest() { + Guid = new Guid(), + Version = "0", + Name = "TEST" + }; } var duplicatePlugin = results.FirstOrDefault(r => r.Manifest.Guid == manifest.Guid); + if (duplicatePlugin != null) { - _log.Warn( - $"The GUID provided by {manifest.Name} ({item}) is already in use by {duplicatePlugin.Manifest.Name}."); + _log.Warn($"The GUID provided by {manifest.Name} ({item}) is already in use by {duplicatePlugin.Manifest.Name}."); continue; } - + if (!Torch.Config.LocalPlugins && !debug) { if (isZip && !Torch.Config.Plugins.Contains(manifest.Guid)) @@ -291,13 +294,13 @@ private List GetLocalPlugins(string pluginDir, bool debug = false) _log.Warn($"Plugin {manifest.Name} ({item}) exists in the plugin directory, but is not listed in torch.cfg. Skipping load!"); continue; } + _log.Info($"First-time load: Plugin {manifest.Name} added to torch.cfg."); Torch.Config.Plugins.Add(manifest.Guid); } } - - results.Add(new PluginItem - { + + results.Add(new PluginItem { Filename = item, IsZip = isZip, Manifest = manifest, @@ -307,14 +310,16 @@ private List GetLocalPlugins(string pluginDir, bool debug = false) if (!Torch.Config.LocalPlugins && firstLoad) Torch.Config.Save(); - + return results; - } - + } + private bool DownloadPluginUpdates(List plugins) { _log.Info("Checking for plugin updates..."); - var count = 0; + + int count = 0; + Task.WaitAll(plugins.Select(async item => { try @@ -324,7 +329,9 @@ private bool DownloadPluginUpdates(List plugins) _log.Warn($"Unzipped plugins cannot be auto-updated. Skipping plugin {item}"); return; } + item.Manifest.Version.TryExtractVersion(out Version currentVersion); + var latest = await Instance.QueryOne(item.Manifest.Guid); if (latest?.LatestVersion == null) @@ -348,6 +355,7 @@ private bool DownloadPluginUpdates(List plugins) } _log.Info($"Updating plugin '{item.Manifest.Name}' from {currentVersion} to {newVersion}."); + await Instance.DownloadPlugin(latest, item.Path); Interlocked.Increment(ref count); } @@ -359,15 +367,15 @@ private bool DownloadPluginUpdates(List plugins) }).ToArray()); _log.Info($"Updated {count} plugins."); + return count > 0; } - + private void LoadPlugin(PluginItem item) { var assemblies = new List(); + //var loaded = AppDomain.CurrentDomain.GetAssemblies(); - var loaded = AppDomain.CurrentDomain.GetAssemblies(); - if (item.IsZip) { using (var zipFile = ZipFile.OpenRead(item.Path)) @@ -380,24 +388,25 @@ private void LoadPlugin(PluginItem item) //if (loaded.Any(a => entry.Name.Contains(a.GetName().Name))) // continue; - using (var stream = entry.Open()) { - var data = stream.ReadToEnd((int) entry.Length); + var data = stream.ReadToEnd((int)entry.Length); byte[] symbol = null; - var symbolEntryName = - entry.FullName.Substring(0, entry.FullName.Length - "dll".Length) + "pdb"; + var symbolEntryName = entry.FullName.Substring(0, entry.FullName.Length - "dll".Length) + "pdb"; var symbolEntry = zipFile.GetEntry(symbolEntryName); + if (symbolEntry != null) + { try { using (var symbolStream = symbolEntry.Open()) - symbol = symbolStream.ReadToEnd((int) symbolEntry.Length); + symbol = symbolStream.ReadToEnd((int)symbolEntry.Length); } catch (Exception e) { _log.Warn(e, $"Failed to read debugging symbols from {item.Filename}:{symbolEntryName}"); } + } assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data)); } @@ -409,7 +418,7 @@ private void LoadPlugin(PluginItem item) var files = Directory .EnumerateFiles(item.Path, "*.*", SearchOption.AllDirectories) .ToList(); - + foreach (var file in files) { if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) @@ -422,9 +431,12 @@ private void LoadPlugin(PluginItem item) { var data = stream.ReadToEnd(); byte[] symbol = null; + var symbolPath = Path.Combine(Path.GetDirectoryName(file) ?? ".", Path.GetFileNameWithoutExtension(file) + ".pdb"); + if (File.Exists(symbolPath)) + { try { using (var symbolStream = File.OpenRead(symbolPath)) @@ -434,46 +446,52 @@ private void LoadPlugin(PluginItem item) { _log.Warn(e, $"Failed to read debugging symbols from {symbolPath}"); } - + } + assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data)); } } - - } - + RegisterAllAssemblies(assemblies); InstantiatePlugin(item.Manifest, assemblies); } - + private void RegisterAllAssemblies(IReadOnlyCollection assemblies) { Assembly ResolveDependentAssembly(object sender, ResolveEventArgs args) { var requiredAssemblyName = new AssemblyName(args.Name); + foreach (Assembly asm in assemblies) { if (IsAssemblyCompatible(requiredAssemblyName, asm.GetName())) return asm; } + if (requiredAssemblyName.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) return null; + foreach (var asm in assemblies) + { if (asm == args.RequestingAssembly) { _log.Warn($"Couldn't find dependency! {args.RequestingAssembly} depends on {requiredAssemblyName}."); break; } + } + return null; } AppDomain.CurrentDomain.AssemblyResolve += ResolveDependentAssembly; + foreach (Assembly asm in assemblies) { TorchBase.RegisterAuxAssembly(asm); } } - + private static bool IsAssemblyCompatible(AssemblyName a, AssemblyName b) { return a.Name == b.Name && a.Version.Major == b.Version.Major && a.Version.Minor == b.Version.Minor; @@ -482,19 +500,19 @@ private static bool IsAssemblyCompatible(AssemblyName a, AssemblyName b) public void ReloadPlugins() { _log.Info("Reloading plugins."); - + var plugins = _plugins.ToList(); if (!Torch.Config.BypassIsReloadableFlag) plugins = plugins.Where(p => p.Value.IsReloadable).ToList(); - + foreach (var plugin in plugins) { _reloadList.Add(plugin.Key); plugin.Value?.Dispose(); _plugins.Remove(plugin.Key); } - + LoadPlugins(); PluginsReloaded?.Invoke(); } @@ -502,7 +520,7 @@ public void ReloadPlugins() public void ReloadPlugin(Guid guid) { var plugin = _plugins[guid]; - + plugin.Dispose(); _plugins.Remove(guid); _log.Info($"{plugin.Name} {plugin.Version} has been unloaded."); @@ -510,11 +528,12 @@ public void ReloadPlugin(Guid guid) LoadPlugin(_pluginItems.First(p => p.Manifest.Guid == guid)); _log.Info($"{plugin.Name} {plugin.Version} has been reloaded."); } - + private void InstantiatePlugin(PluginManifest manifest, IEnumerable assemblies) { Type pluginType = null; bool mult = false; + foreach (var asm in assemblies) { foreach (var type in asm.GetExportedTypes()) @@ -553,11 +572,14 @@ private void InstantiatePlugin(PluginManifest manifest, IEnumerable as // Backwards compatibility for PluginAttribute. var pluginAttr = pluginType.GetCustomAttribute(); + if (pluginAttr != null) { _log.Warn($"Plugin '{manifest.Name}' is using the obsolete {nameof(PluginAttribute)}, using info from attribute if necessary."); + manifest.Version = manifest.Version ?? pluginAttr.Version.ToString(); manifest.Name = manifest.Name ?? pluginAttr.Name; + if (manifest.Guid == default(Guid)) manifest.Guid = pluginAttr.Guid; } @@ -565,6 +587,7 @@ private void InstantiatePlugin(PluginManifest manifest, IEnumerable as _log.Info($"Loading plugin '{manifest.Name}' ({manifest.Version})"); TorchPluginBase plugin; + try { plugin = (TorchPluginBase)Activator.CreateInstance(pluginType); @@ -574,12 +597,13 @@ private void InstantiatePlugin(PluginManifest manifest, IEnumerable as _log.Error(ex, $"Plugin {manifest.Name} threw an exception during instantiation! Not loading!"); return; } + plugin.Manifest = manifest; plugin.StoragePath = Torch.Config.InstancePath; plugin.Torch = Torch; _plugins.Add(manifest.Guid, plugin); } - + private PluginManifest GetManifestFromZip(string path) { try @@ -611,11 +635,11 @@ private bool TryValidatePluginDependencies(List items, ref PluginIte { var dependencies = new List(); missingDependencies = new List(); - + foreach (var pluginDependency in item.Manifest.Dependencies) { - var dependency = items - .FirstOrDefault(pi => pi?.Manifest.Guid == pluginDependency.Plugin); + var dependency = items.FirstOrDefault(pi => pi?.Manifest.Guid == pluginDependency.Plugin); + if (dependency == null) { missingDependencies.Add(pluginDependency.Plugin); @@ -631,7 +655,7 @@ private bool TryValidatePluginDependencies(List items, ref PluginIte { // If dependency version is too low, we can try to update. Otherwise // it's a missing dependency. - + // For now let's just warn the user. bitMuse is lazy. _log.Warn($"{dependency.Manifest.Name} is below the requested version for {item.Manifest.Name}." + Environment.NewLine @@ -645,8 +669,10 @@ private bool TryValidatePluginDependencies(List items, ref PluginIte } item.ResolvedDependencies = dependencies; + if (missingDependencies.Count > 0) return false; + return true; } From d7d624592ee6f7e8a1bd89479f3b736608b44bcf Mon Sep 17 00:00:00 2001 From: Remaarn Date: Tue, 9 Sep 2025 16:28:30 +1000 Subject: [PATCH 2/4] Improve MiscExtensions.ReadToEnd to fix exception when trying to get Length of a compressed stream. --- Torch/Utils/MiscExtensions.cs | 62 ++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/Torch/Utils/MiscExtensions.cs b/Torch/Utils/MiscExtensions.cs index 46078dc6..1dca31dd 100644 --- a/Torch/Utils/MiscExtensions.cs +++ b/Torch/Utils/MiscExtensions.cs @@ -1,8 +1,6 @@ using System; using System.IO; -using System.Linq; using System.Net; -using System.Threading; using Sandbox.Engine.Multiplayer; using Sandbox.Game.Entities; using Sandbox.Game.World; @@ -12,52 +10,56 @@ namespace Torch.Utils { public static class MiscExtensions { - private static readonly ThreadLocal> _streamBuffer = new ThreadLocal>(() => new WeakReference(null)); - - private static long LengthSafe(this Stream stream) + public static byte[] ReadToEnd(this Stream stream, int optionalDataLength = -1) { + long streamLength = optionalDataLength; + try { - return stream.Length; + if (stream.CanSeek) + streamLength = stream.Length; } catch { - return 512; } - } - public static byte[] ReadToEnd(this Stream stream, int optionalDataLength = -1) - { - byte[] buffer; - if (!_streamBuffer.Value.TryGetTarget(out buffer)) - buffer = new byte[stream.LengthSafe()]; - var initialBufferSize = optionalDataLength > 0 ? optionalDataLength : stream.LengthSafe(); - if (buffer.Length < initialBufferSize) - buffer = new byte[initialBufferSize]; - if (buffer.Length < 1024) - buffer = new byte[1024]; - var streamPosition = 0; - while (true) + int bufferSize = streamLength > 0 ? (int)streamLength : 32768; + var buffer = new byte[bufferSize]; + int totalRead = 0; + int numRead; + + do { - if (buffer.Length == streamPosition) - Array.Resize(ref buffer, Math.Max((int)stream.LengthSafe(), buffer.Length * 2)); - int count = stream.Read(buffer, streamPosition, buffer.Length - streamPosition); - if (count == 0) - break; + int toRead = buffer.Length - totalRead; - streamPosition += count; + numRead = stream.Read(buffer, totalRead, toRead); + + totalRead += numRead; + + if (numRead > 0 && totalRead == buffer.Length) + { + if (streamLength > 0) + break; + + Array.Resize(ref buffer, buffer.Length * 2); + } } + while (numRead > 0); + + var result = new byte[totalRead]; - var result = new byte[streamPosition]; Array.Copy(buffer, 0, result, 0, result.Length); - _streamBuffer.Value.SetTarget(buffer); + return result; } public static IPAddress GetRemoteIP(this P2PSessionState_t state) { - // What is endianness anyway? - return new IPAddress(BitConverter.GetBytes(state.m_nRemoteIP).Reverse().ToArray()); + // Reverse endianness + var bytes = BitConverter.GetBytes(state.m_nRemoteIP); + Array.Reverse(bytes); + + return new IPAddress(bytes); } public static string GetGridOwnerName(this MyCubeGrid grid) From 839fa7e88f3179bb04009f1ef97dbf8b62a7a5e5 Mon Sep 17 00:00:00 2001 From: Remaarn Date: Tue, 9 Sep 2025 16:29:10 +1000 Subject: [PATCH 3/4] Prevent exception when creating ModItem in TorchSessionManager constructor --- Torch/Session/TorchSessionManager.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Torch/Session/TorchSessionManager.cs b/Torch/Session/TorchSessionManager.cs index 14053cfd..0c6f71b3 100644 --- a/Torch/Session/TorchSessionManager.cs +++ b/Torch/Session/TorchSessionManager.cs @@ -1,18 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using NLog; -using Sandbox.Engine.Networking; using Sandbox.Game.World; using Torch.API; using Torch.API.Managers; using Torch.API.Session; using Torch.Managers; using Torch.Mod; -using Torch.Session; using Torch.Utils; using VRage.Game; @@ -46,8 +41,9 @@ public class TorchSessionManager : Manager, ITorchSessionManager public TorchSessionManager(ITorchBase torchInstance) : base(torchInstance) { _overrideMods = new Dictionary(); + if (Torch.Config.UgcServiceType == UGCServiceType.Steam) - _overrideMods.Add(TorchModCore.MOD_ID, ModItemUtils.Create(TorchModCore.MOD_ID)); + _overrideMods.Add(TorchModCore.MOD_ID, ModItemUtils.Create(TorchModCore.MOD_ID, "Steam")); } /// @@ -81,7 +77,7 @@ public bool AddOverrideMod(ulong modId) /// public bool RemoveOverrideMod(ulong modId) { - if(_overrideMods.TryGetValue(modId, out var item)) + if (_overrideMods.TryGetValue(modId, out var item)) OverrideModsChanged?.Invoke(new CollectionChangeEventArgs(CollectionChangeAction.Remove, item)); return _overrideMods.Remove(modId); From ea23b273d4b2ec6398532f38ee405b9ebaf04f07 Mon Sep 17 00:00:00 2001 From: Remaarn Date: Fri, 31 Oct 2025 17:18:10 +1000 Subject: [PATCH 4/4] Fix issue #610 --- Torch/Plugins/PluginManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Torch/Plugins/PluginManager.cs b/Torch/Plugins/PluginManager.cs index 61ffcb04..5421c9a4 100644 --- a/Torch/Plugins/PluginManager.cs +++ b/Torch/Plugins/PluginManager.cs @@ -326,7 +326,7 @@ private bool DownloadPluginUpdates(List plugins) { if (!item.IsZip) { - _log.Warn($"Unzipped plugins cannot be auto-updated. Skipping plugin {item}"); + _log.Warn($"Unzipped plugins cannot be auto-updated. Skipping plugin '{item.Manifest.Name}'"); return; } @@ -336,7 +336,7 @@ private bool DownloadPluginUpdates(List plugins) if (latest?.LatestVersion == null) { - _log.Warn($"Plugin {item.Manifest.Name} does not have any releases on torchapi.com. Cannot update."); + _log.Warn($"Plugin '{item.Manifest.Name}' does not have any releases on torchapi.com. Cannot update."); return; } @@ -361,7 +361,7 @@ private bool DownloadPluginUpdates(List plugins) } catch (Exception e) { - _log.Warn($"An error occurred updating the plugin {item.Manifest.Name}."); + _log.Warn($"An error occurred updating the plugin '{item.Manifest.Name}'."); _log.Warn(e); } }).ToArray());