Skip to content

Commit ef23a63

Browse files
Staging (#566)
* First pass at plugin 'reload' system. Main benefit will be to those who rent from a provider and want to make config changes on the fly without restarting the server. (#561) * Plugin hot reloading (#563) * First pass at plugin 'reload' system. Main benefit will be to those who rent from a provider and want to make config changes on the fly without restarting the server. * Flag for plugins authors to set if their plugin supports reloading or not (false by default) * Plugin hot reloading (#564) * First pass at plugin 'reload' system. Main benefit will be to those who rent from a provider and want to make config changes on the fly without restarting the server. * Flag for plugins authors to set if their plugin supports reloading or not (false by default) * Add new flag to bypass check to see if a plugin supports reloading or not. * Fix autosave occurring when user specified to not save. (#565) * Fix for world saving even when the user specified to not save with !stop or !restart. Also fixed typo. * Made AutoSavePatch internal --------- Co-authored-by: WesternGamer <80211714+WesternGamer@users.noreply.github.com>
1 parent 82faa02 commit ef23a63

11 files changed

Lines changed: 219 additions & 10 deletions

File tree

‎Torch.API/ITorchConfig.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public interface ITorchConfig
1818
bool LocalPlugins { get; set; }
1919
bool RestartOnCrash { get; set; }
2020
bool ShouldUpdatePlugins { get; }
21+
bool BypassIsReloadableFlag { get; set; }
2122
bool ShouldUpdateTorch { get; }
2223
int TickTimeout { get; set; }
2324
string WaitForPID { get; set; }

‎Torch.API/Plugins/ITorchPlugin.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public interface ITorchPlugin : IDisposable
2323
/// The name of the plugin.
2424
/// </summary>
2525
string Name { get; }
26+
27+
/// <summary>
28+
/// Enable/Disable Plugin reloading
29+
/// </summary>
30+
bool IsReloadable { get; set; }
2631

2732
/// <summary>
2833
/// This is called before the game loop is started.

‎Torch.Server/TorchConfig.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class TorchConfig : CommandLine, ITorchConfig, INotifyPropertyChanged
4747
private bool _sendLogsToKeen;
4848
private bool _deleteMiniDumps = true;
4949
private string _loginToken;
50+
private bool bypassIsReloadableFlag;
5051

5152

5253
/// <inheritdoc />
@@ -119,6 +120,10 @@ public string InstancePath
119120
/// <inheritdoc />
120121
[Display(Name = "Update Plugins", Description = "Check every start for new versions of plugins.", GroupName = "Server")]
121122
public bool GetPluginUpdates { get => _getPluginUpdates; set => Set(value, ref _getPluginUpdates); }
123+
124+
/// <inheritdoc />
125+
[Display(Name = "Bypass reloadable flag", Description = "Bypass the reloadable flag on plugins (forces true).", GroupName = "Server")]
126+
public bool BypassIsReloadableFlag { get => bypassIsReloadableFlag; set => Set(value, ref bypassIsReloadableFlag); }
122127

123128
/// <inheritdoc />
124129
[Display(Name = "Watchdog Timeout", Description = "Watchdog timeout (in seconds).", GroupName = "Server")]

‎Torch.Server/ViewModels/PluginViewModel.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public PluginViewModel(ITorchPlugin plugin)
3131
{
3232
Control = p.GetControl();
3333
}
34+
catch(InvalidOperationException ex)
35+
{
36+
//ignore as its likely a hot reload, we can figure out a better solution in the future.
37+
Control = null;
38+
}
3439
catch (Exception ex)
3540
{
3641
_log.Error(ex, $"Exception loading interface for plugin {Plugin.Name}! Plugin interface will not be available!");

‎Torch.Server/Views/PluginsControl.xaml.cs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ public void BindServer(ITorchServer server)
5454
{
5555
_server = server;
5656
_server.Initialized += Server_Initialized;
57+
_server.Managers.GetManager<PluginManager>().PluginsReloaded += PluginsReloaded;
5758
}
5859

59-
private void Server_Initialized(ITorchServer obj)
60+
private void Server_Initialized(ITorchServer obj = null)
6061
{
6162
Dispatcher.InvokeAsync(() =>
6263
{
@@ -65,7 +66,11 @@ private void Server_Initialized(ITorchServer obj)
6566
DataContext = pluginManager;
6667
pluginManager.PropertyChanged += PluginManagerOnPropertyChanged;
6768
});
69+
}
6870

71+
private void PluginsReloaded()
72+
{
73+
Server_Initialized();
6974
}
7075

7176
private void OpenFolder_OnClick(object sender, RoutedEventArgs e)

‎Torch/Commands/TorchCommands.cs‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using Torch.Managers;
2121
using Torch.Mod;
2222
using Torch.Mod.Messages;
23+
using Torch.Patches;
2324
using VRage.Game;
2425
using VRage.Game.ModAPI;
2526

@@ -176,6 +177,43 @@ public void Version()
176177
var ver = Context.Torch.TorchVersion;
177178
Context.Respond($"Torch version: {ver} SE version: {MyFinalBuildConstants.APP_VERSION}");
178179
}
180+
181+
[Command("reload", "Reloads a specified plugin or all plugins if none specified.")]
182+
[Permission(MyPromoteLevel.Admin)]
183+
public void Reload(string plugin = null)
184+
{
185+
var pluginManager = Context.Torch.Managers.GetManager<PluginManager>();
186+
if (pluginManager == null)
187+
{
188+
Context.Respond("Plugin manager not found.");
189+
return;
190+
}
191+
192+
if (string.IsNullOrEmpty(plugin))
193+
{
194+
pluginManager.ReloadPlugins();
195+
Context.Respond("Reloaded all plugins.");
196+
}
197+
else
198+
{
199+
//find plugin by name
200+
var pluginToReload = pluginManager.Plugins.Values.FirstOrDefault(p => p.Name.Equals(plugin, StringComparison.InvariantCultureIgnoreCase));
201+
if (pluginToReload == null) //not found
202+
{
203+
Context.Respond($"Plugin {plugin} not found.");
204+
return;
205+
}
206+
207+
if (!pluginToReload.IsReloadable && !Context.Torch.Config.BypassIsReloadableFlag)
208+
{
209+
Context.Respond($"{pluginToReload.Name} does not support reloading.");
210+
}
211+
212+
213+
pluginManager.ReloadPlugin(pluginToReload.Id);
214+
Context.Respond($"Reloaded plugin");
215+
}
216+
}
179217

180218
[Command("plugins", "Lists the currently loaded plugins.")]
181219
[Permission(MyPromoteLevel.None)]
@@ -292,6 +330,7 @@ private IEnumerable StopCountdown(int countdown, bool save)
292330
}
293331
else
294332
{
333+
AutoSavePatch.SaveFromCommand = true;
295334
if (save)
296335
{
297336
Log.Info("Saving game before stop.");
@@ -345,9 +384,10 @@ private IEnumerable RestartCountdown(int countdown, bool save)
345384
}
346385
else
347386
{
387+
AutoSavePatch.SaveFromCommand = true;
348388
if (save)
349389
{
350-
Log.Info("Savin game before restart.");
390+
Log.Info("Saving game before restart.");
351391
Context.Torch.CurrentSession.Managers.GetManager<IChatManagerClient>()
352392
.SendMessageAsSelf($"Saving game before restart.");
353393
}

‎Torch/Managers/PatchManager/PatchManager.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ public override void Detach()
209209
{
210210
lock (_contexts)
211211
{
212+
_log.Info("Removing all patches...");
212213
foreach (List<PatchContext> set in _contexts.Values)
213214
foreach (PatchContext ctx in set)
214215
ctx.RemoveAll();

‎Torch/Patches/AutoSavePatch.cs‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using NLog;
2+
using Sandbox;
3+
using Sandbox.Game.World;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Reflection;
7+
using System.Reflection.Emit;
8+
using Torch.Managers.PatchManager;
9+
using Torch.Managers.PatchManager.MSIL;
10+
11+
namespace Torch.Patches
12+
{
13+
/*
14+
The purpose of this patch is to prevent a autosave from occurring unintentionally during world unload initiated by the !stop or !restart command.
15+
Due to the user using the command(s) potentally performing a autosave or an opting out of a autosave, Keen's autosave code during unload has to be disabled.
16+
Setting MySandboxGame.ConfigDedicated.RestartSave to false can resolve this issue, but this method (patch method) prevents us from changing the config file in order to prevent confusion.
17+
*/
18+
[PatchShim]
19+
internal static class AutoSavePatch
20+
{
21+
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
22+
23+
/// <summary>
24+
/// If set to true, specifies that the session is being unloaded from a torch ingame chat command and that saving is being handled by the command.
25+
/// </summary>
26+
public static bool SaveFromCommand { get; set; } = false;
27+
28+
public static void Patch(PatchContext ctx)
29+
{
30+
var transpiler = typeof(AutoSavePatch).GetMethod(nameof(Transpile), BindingFlags.NonPublic | BindingFlags.Static);
31+
ctx.GetPattern(typeof(MySession).GetMethod("Unload", BindingFlags.Public | BindingFlags.Instance))
32+
.Transpilers.Add(transpiler);
33+
_log.Info("Patching autosave on unload.");
34+
}
35+
36+
private static IEnumerable<MsilInstruction> Transpile(IEnumerable<MsilInstruction> instructions)
37+
{
38+
var msil = instructions.ToList();
39+
40+
for (var i = 0; i < msil.Count; i++)
41+
{
42+
var instruction = msil[i];
43+
if (instruction.OpCode == OpCodes.Ldsfld && instruction.Operand is MsilOperandInline.MsilOperandReflected<FieldInfo> operandReflected
44+
&& operandReflected.Value.FieldType == typeof(bool) && operandReflected.Value.Name == "IsDedicated")
45+
{
46+
for (int c = 0; c < 13; c++)
47+
{
48+
msil.RemoveAt(i);
49+
}
50+
51+
var call = new MsilInstruction(OpCodes.Call);
52+
(call.Operand as MsilOperandInline.MsilOperandReflected<MethodBase>).Value = typeof(AutoSavePatch).GetMethod(nameof(SaveIfNeeded), BindingFlags.NonPublic | BindingFlags.Static);
53+
msil.Insert(i, call);
54+
55+
break;
56+
}
57+
}
58+
59+
return msil;
60+
}
61+
62+
// Reimplementation of Keen's autosaving code during world unload with SaveFromCommand check and with save status being outputed to console.
63+
private static void SaveIfNeeded()
64+
{
65+
if (SaveFromCommand)
66+
{
67+
return;
68+
}
69+
70+
if (Sandbox.Engine.Platform.Game.IsDedicated && MySandboxGame.ConfigDedicated.RestartSave)
71+
{
72+
_log.Info("Autosaving during world unloading.");
73+
// We have to use the vanilla implementation as the torch implementation does not work in Sandbox.Game.World.MySession:Unload()
74+
bool result = MySession.Static.Save();
75+
76+
if (result)
77+
{
78+
_log.Info("Autosave successful.");
79+
}
80+
else
81+
{
82+
_log.Warn("Autosave failed.");
83+
}
84+
}
85+
}
86+
}
87+
}

‎Torch/Plugins/PluginManager.cs‎

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
using System.Reflection;
1010
using System.Threading;
1111
using System.Threading.Tasks;
12+
using System.Windows;
1213
using System.Xml.Serialization;
14+
using Havok;
1315
using NLog;
1416
using Torch.API;
1517
using Torch.API.Managers;
@@ -25,6 +27,9 @@ namespace Torch.Managers
2527
/// <inheritdoc />
2628
public class PluginManager : Manager, IPluginManager
2729
{
30+
31+
//event for when the plugins are reloaded
32+
public event Action PluginsReloaded;
2833
private class PluginItem
2934
{
3035
public string Filename { get; set; }
@@ -40,6 +45,8 @@ private class PluginItem
4045

4146
public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
4247
private readonly MtObservableSortedDictionary<Guid, ITorchPlugin> _plugins = new MtObservableSortedDictionary<Guid, ITorchPlugin>();
48+
private readonly List<PluginItem> _pluginItems = new List<PluginItem>();
49+
private readonly List<Guid> _reloadList = new List<Guid>();
4350
private CommandManager _mgr;
4451

4552
#pragma warning disable 649
@@ -192,17 +199,36 @@ public void LoadPlugins()
192199
// This will happen on cylic dependencies.
193200
_log.Error(e);
194201
}
195-
196-
// Actually load the plugins now.
197-
foreach (var item in pluginsToLoad)
202+
203+
if (_reloadList.Count > 0)
198204
{
199-
LoadPlugin(item);
200-
}
201-
202-
foreach (var plugin in _plugins.Values)
205+
foreach (var item in _pluginItems)
206+
{
207+
LoadPlugin(item);
208+
}
209+
210+
foreach (var plugin in _plugins.Values)
211+
{
212+
plugin.Init(Torch);
213+
}
214+
}
215+
else
203216
{
204-
plugin.Init(Torch);
217+
foreach (var plugin in pluginsToLoad)
218+
{
219+
_pluginItems.Add(plugin);
220+
LoadPlugin(plugin);
221+
}
222+
223+
foreach (var plugin in _plugins.Values)
224+
{
225+
plugin.Init(Torch);
226+
}
205227
}
228+
229+
_reloadList.Clear();
230+
231+
206232
_log.Info($"Loaded {_plugins.Count} plugins.");
207233
PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly());
208234
}
@@ -444,6 +470,38 @@ private static bool IsAssemblyCompatible(AssemblyName a, AssemblyName b)
444470
{
445471
return a.Name == b.Name && a.Version.Major == b.Version.Major && a.Version.Minor == b.Version.Minor;
446472
}
473+
474+
public void ReloadPlugins()
475+
{
476+
_log.Info("Reloading plugins.");
477+
478+
var plugins = _plugins.ToList();
479+
480+
if (!Torch.Config.BypassIsReloadableFlag)
481+
plugins = plugins.Where(p => p.Value.IsReloadable).ToList();
482+
483+
foreach (var plugin in plugins)
484+
{
485+
_reloadList.Add(plugin.Key);
486+
plugin.Value?.Dispose();
487+
_plugins.Remove(plugin.Key);
488+
}
489+
490+
LoadPlugins();
491+
PluginsReloaded?.Invoke();
492+
}
493+
494+
public void ReloadPlugin(Guid guid)
495+
{
496+
var plugin = _plugins[guid];
497+
498+
plugin.Dispose();
499+
_plugins.Remove(guid);
500+
_log.Info($"{plugin.Name} {plugin.Version} has been unloaded.");
501+
502+
LoadPlugin(_pluginItems.First(p => p.Manifest.Guid == guid));
503+
_log.Info($"{plugin.Name} {plugin.Version} has been reloaded.");
504+
}
447505

448506
private void InstantiatePlugin(PluginManifest manifest, IEnumerable<Assembly> assemblies)
449507
{

‎Torch/Torch.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@
240240
<Compile Include="Managers\PatchManager\Transpile\MethodContext.cs" />
241241
<Compile Include="Managers\PatchManager\Transpile\MethodTranspiler.cs" />
242242
<Compile Include="MySteamServiceWrapper.cs" />
243+
<Compile Include="Patches\AutoSavePatch.cs" />
243244
<Compile Include="Patches\GameAnalyticsPatch.cs" />
244245
<Compile Include="Patches\GameStatePatchShim.cs" />
245246
<Compile Include="Patches\KeenLogPatch.cs" />

0 commit comments

Comments
 (0)