Make plugins load asynchronously (#896)

This commit is contained in:
kizer 2022-06-25 21:12:46 +09:00 committed by GitHub
parent 19eb54cd78
commit 7760457dc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 204 additions and 60 deletions

View file

@ -27,7 +27,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Lumina" Version="3.7.0" /> <PackageReference Include="Lumina" Version="3.8.0" />
<PackageReference Include="Lumina.Excel" Version="6.1.1" /> <PackageReference Include="Lumina.Excel" Version="6.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333"> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">

View file

@ -65,7 +65,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CheapLoc" Version="1.1.6" /> <PackageReference Include="CheapLoc" Version="1.1.6" />
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" /> <PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
<PackageReference Include="Lumina" Version="3.7.0" /> <PackageReference Include="Lumina" Version="3.8.0" />
<PackageReference Include="Lumina.Excel" Version="6.1.1" /> <PackageReference Include="Lumina.Excel" Version="6.1.1" />
<PackageReference Include="MinSharp" Version="1.0.4" /> <PackageReference Include="MinSharp" Version="1.0.4" />
<PackageReference Include="MonoMod.RuntimeDetour" Version="21.10.10.01" /> <PackageReference Include="MonoMod.RuntimeDetour" Version="21.10.10.01" />

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using JetBrains.Annotations;
namespace Dalamud.Game namespace Dalamud.Game
{ {
@ -21,7 +22,16 @@ namespace Dalamud.Game
protected bool IsResolved { get; set; } protected bool IsResolved { get; set; }
/// <summary> /// <summary>
/// Setup the resolver, calling the appopriate method based on the process architecture. /// Setup the resolver, calling the appropriate method based on the process architecture,
/// using the default SigScanner.
///
/// For plugins. Not intended to be called from Dalamud Service{T} constructors.
/// </summary>
[UsedImplicitly]
public void Setup() => this.Setup(Service<SigScanner>.Get());
/// <summary>
/// Setup the resolver, calling the appropriate method based on the process architecture.
/// </summary> /// </summary>
/// <param name="scanner">The SigScanner instance.</param> /// <param name="scanner">The SigScanner instance.</param>
public void Setup(SigScanner scanner) public void Setup(SigScanner scanner)

View file

@ -5,7 +5,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Windows.Forms;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Utility.Timing; using Dalamud.Utility.Timing;
@ -340,10 +340,16 @@ namespace Dalamud.Game
/// <returns>The real offset of the found signature.</returns> /// <returns>The real offset of the found signature.</returns>
public IntPtr ScanText(string signature) public IntPtr ScanText(string signature)
{ {
if (this.textCache != null && this.textCache.TryGetValue(signature, out var address)) if (this.textCache != null)
{
lock (this.textCache)
{
if (this.textCache.TryGetValue(signature, out var address))
{ {
return new IntPtr(address + this.Module.BaseAddress.ToInt64()); return new IntPtr(address + this.Module.BaseAddress.ToInt64());
} }
}
}
var mBase = this.IsCopy ? this.moduleCopyPtr : this.TextSectionBase; var mBase = this.IsCopy ? this.moduleCopyPtr : this.TextSectionBase;
var scanRet = Scan(mBase, this.TextSectionSize, signature); var scanRet = Scan(mBase, this.TextSectionSize, signature);
@ -356,7 +362,13 @@ namespace Dalamud.Game
if (insnByte == 0xE8 || insnByte == 0xE9) if (insnByte == 0xE8 || insnByte == 0xE9)
scanRet = ReadJmpCallSig(scanRet); scanRet = ReadJmpCallSig(scanRet);
this.textCache?.Add(signature, scanRet.ToInt64() - this.Module.BaseAddress.ToInt64()); if (this.textCache != null)
{
lock (this.textCache)
{
this.textCache[signature] = scanRet.ToInt64() - this.Module.BaseAddress.ToInt64();
}
}
return scanRet; return scanRet;
} }

View file

@ -120,11 +120,6 @@ namespace Dalamud.Interface.Internal
private delegate void InstallRTSSHook(); private delegate void InstallRTSSHook();
/// <summary>
/// Gets a task that gets completed when scene gets initialized.
/// </summary>
public Task SceneInitializeTask => this.sceneInitializeTaskCompletionSource.Task;
/// <summary> /// <summary>
/// This event gets called each frame to facilitate ImGui drawing. /// This event gets called each frame to facilitate ImGui drawing.
/// </summary> /// </summary>
@ -165,6 +160,11 @@ namespace Dalamud.Interface.Internal
/// </summary> /// </summary>
public static ImFontPtr MonoFont { get; private set; } public static ImFontPtr MonoFont { get; private set; }
/// <summary>
/// Gets a task that gets completed when scene gets initialized.
/// </summary>
public Task SceneInitializeTask => this.sceneInitializeTaskCompletionSource.Task;
/// <summary> /// <summary>
/// Gets or sets the pointer to ImGui.IO(), when it was last used. /// Gets or sets the pointer to ImGui.IO(), when it was last used.
/// </summary> /// </summary>
@ -448,7 +448,6 @@ namespace Dalamud.Interface.Internal
try try
{ {
this.scene = new RawDX11Scene(swapChain); this.scene = new RawDX11Scene(swapChain);
this.sceneInitializeTaskCompletionSource.SetResult();
} }
catch (DllNotFoundException ex) catch (DllNotFoundException ex)
{ {
@ -568,11 +567,17 @@ namespace Dalamud.Interface.Internal
this.RenderImGui(); this.RenderImGui();
if (!this.SceneInitializeTask.IsCompleted)
this.sceneInitializeTaskCompletionSource.SetResult();
return pRes; return pRes;
} }
this.RenderImGui(); this.RenderImGui();
if (!this.SceneInitializeTask.IsCompleted)
this.sceneInitializeTaskCompletionSource.SetResult();
return this.presentHook.Original(swapChain, syncInterval, presentFlags); return this.presentHook.Original(swapChain, syncInterval, presentFlags);
} }

View file

@ -103,7 +103,9 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
MaximumSize = new Vector2(5000, 5000), MaximumSize = new Vector2(5000, 5000),
}; };
var pluginManager = Service<PluginManager>.Get(); Service<PluginManager>.GetAsync().ContinueWith(pluginManagerTask =>
{
var pluginManager = pluginManagerTask.Result;
// For debugging // For debugging
if (pluginManager.PluginsReady) if (pluginManager.PluginsReady)
@ -116,6 +118,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
{ {
this.testerImagePaths[i] = string.Empty; this.testerImagePaths[i] = string.Empty;
} }
});
} }
private enum OperationStatus private enum OperationStatus

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
@ -16,10 +17,13 @@ using Dalamud.Game;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal; namespace Dalamud.Plugin.Internal;
@ -37,6 +41,7 @@ internal partial class PluginManager : IDisposable
private static readonly ModuleLog Log = new("PLUGINM"); private static readonly ModuleLog Log = new("PLUGINM");
private readonly object pluginListLock = new();
private readonly DirectoryInfo pluginDirectory; private readonly DirectoryInfo pluginDirectory;
private readonly DirectoryInfo devPluginDirectory; private readonly DirectoryInfo devPluginDirectory;
private readonly BannedPlugin[] bannedPlugins; private readonly BannedPlugin[] bannedPlugins;
@ -310,13 +315,18 @@ internal partial class PluginManager : IDisposable
// Dev plugins should load first. // Dev plugins should load first.
pluginDefs.InsertRange(0, devPluginDefs); pluginDefs.InsertRange(0, devPluginDefs);
void LoadPlugins(IEnumerable<PluginDef> pluginDefsList) void LoadPluginOnBoot(string logPrefix, PluginDef pluginDef)
{ {
foreach (var pluginDef in pluginDefsList) using (Timings.Start($"{pluginDef.DllFile.Name}: {logPrefix}Boot"))
{ {
try try
{ {
this.LoadPlugin(pluginDef.DllFile, pluginDef.Manifest, PluginLoadReason.Boot, pluginDef.IsDev, isBoot: true); this.LoadPlugin(
pluginDef.DllFile,
pluginDef.Manifest,
PluginLoadReason.Boot,
pluginDef.IsDev,
isBoot: true);
} }
catch (InvalidPluginException) catch (InvalidPluginException)
{ {
@ -324,25 +334,93 @@ internal partial class PluginManager : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "During boot plugin load, an unexpected error occurred"); Log.Error(ex, "{0}: During boot plugin load, an unexpected error occurred", logPrefix);
} }
} }
} }
// Load sync plugins void LoadPluginsSync(string logPrefix, IEnumerable<PluginDef> pluginDefsList)
var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadPriority > 0); {
LoadPlugins(syncPlugins); foreach (var pluginDef in pluginDefsList)
LoadPluginOnBoot(logPrefix, pluginDef);
}
var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0); Task LoadPluginsAsync(string logPrefix, IEnumerable<PluginDef> pluginDefsList)
Task.Run(() => LoadPlugins(asyncPlugins)) {
.ContinueWith(_ => return Task.WhenAll(
pluginDefsList
.Select(pluginDef => Task.Run(() => LoadPluginOnBoot(logPrefix, pluginDef)))
.ToArray());
}
var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadSync == true).ToList();
var asyncPlugins = pluginDefs.Where(def => def.Manifest?.LoadSync != true).ToList();
var loadTasks = new List<Task>();
// Load plugins that can be loaded anytime
LoadPluginsSync(
"AnytimeSync",
syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 2));
loadTasks.Add(
LoadPluginsAsync(
"AnytimeAsync",
asyncPlugins.Where(def => def.Manifest?.LoadRequiredState == 2)));
// Load plugins that want to be loaded during Framework.Tick
loadTasks.Add(
Service<Framework>
.GetAsync()
.ContinueWith(
x => x.Result.RunOnTick(
() => LoadPluginsSync(
"FrameworkTickSync",
syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1))),
TaskContinuationOptions.RunContinuationsAsynchronously)
.Unwrap()
.ContinueWith(
_ => LoadPluginsAsync(
"FrameworkTickAsync",
asyncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1)),
TaskContinuationOptions.RunContinuationsAsynchronously)
.Unwrap());
// Load plugins that want to be loaded during Framework.Tick, when drawing facilities are available
loadTasks.Add(
Service<InterfaceManager>
.GetAsync()
.ContinueWith(
x => x.Result.SceneInitializeTask,
TaskContinuationOptions.RunContinuationsAsynchronously)
.Unwrap()
.ContinueWith(
_ => Service<Framework>.Get().RunOnTick(() =>
{
LoadPluginsSync(
"DrawAvailableSync",
syncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null));
return LoadPluginsAsync(
"DrawAvailableAsync",
asyncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null));
}))
.Unwrap());
// Save signatures when all plugins are done loading, successful or not.
_ = Task
.WhenAll(loadTasks)
.ContinueWith(
_ => Service<SigScanner>.GetAsync(),
TaskContinuationOptions.RunContinuationsAsynchronously)
.Unwrap()
.ContinueWith(
sigScannerTask =>
{ {
this.PluginsReady = true; this.PluginsReady = true;
this.NotifyInstalledPluginsChanged(); this.NotifyInstalledPluginsChanged();
// Save signatures, makes sense to do it here since all plugins will be loaded sigScannerTask.Result.Save();
Service<SigScanner>.Get().Save(); },
}); TaskContinuationOptions.RunContinuationsAsynchronously)
.ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -352,6 +430,8 @@ internal partial class PluginManager : IDisposable
{ {
var aggregate = new List<Exception>(); var aggregate = new List<Exception>();
lock (this.pluginListLock)
{
foreach (var plugin in this.InstalledPlugins) foreach (var plugin in this.InstalledPlugins)
{ {
if (plugin.IsLoaded) if (plugin.IsLoaded)
@ -368,6 +448,7 @@ internal partial class PluginManager : IDisposable
} }
} }
} }
}
if (aggregate.Any()) if (aggregate.Any())
{ {
@ -444,8 +525,11 @@ internal partial class PluginManager : IDisposable
foreach (var dllFile in devDllFiles) foreach (var dllFile in devDllFiles)
{ {
// This file is already known to us // This file is already known to us
lock (this.pluginListLock)
{
if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName)) if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName))
continue; continue;
}
// Manifests are not required for devPlugins. the Plugin type will handle any null manifests. // Manifests are not required for devPlugins. the Plugin type will handle any null manifests.
var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
@ -624,7 +708,7 @@ internal partial class PluginManager : IDisposable
} }
catch (InvalidPluginException) catch (InvalidPluginException)
{ {
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw; throw;
} }
catch (BannedPluginException) catch (BannedPluginException)
@ -647,13 +731,17 @@ internal partial class PluginManager : IDisposable
} }
else else
{ {
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw; throw;
} }
} }
} }
lock (this.pluginListLock)
{
this.InstalledPlugins = this.InstalledPlugins.Add(plugin); this.InstalledPlugins = this.InstalledPlugins.Add(plugin);
}
return plugin; return plugin;
} }
@ -666,8 +754,12 @@ internal partial class PluginManager : IDisposable
if (plugin.State != PluginState.Unloaded) if (plugin.State != PluginState.Unloaded)
throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded"); throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded");
lock (this.pluginListLock)
{
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); }
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
this.NotifyInstalledPluginsChanged(); this.NotifyInstalledPluginsChanged();
this.NotifyAvailablePluginsChanged(); this.NotifyAvailablePluginsChanged();
@ -834,8 +926,11 @@ internal partial class PluginManager : IDisposable
try try
{ {
plugin.DllFile.Delete(); plugin.DllFile.Delete();
lock (this.pluginListLock)
{
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
} }
}
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Error during delete (update)"); Log.Error(ex, "Error during delete (update)");
@ -848,8 +943,11 @@ internal partial class PluginManager : IDisposable
try try
{ {
plugin.Disable(); plugin.Disable();
lock (this.pluginListLock)
{
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
} }
}
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Error during disable (update)"); Log.Error(ex, "Error during disable (update)");
@ -1030,7 +1128,7 @@ internal partial class PluginManager
/// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading
/// plugins via byte[]. /// plugins via byte[].
/// </summary> /// </summary>
internal static readonly Dictionary<string, PluginPatchData> PluginLocations = new(); internal static readonly ConcurrentDictionary<string, PluginPatchData> PluginLocations = new();
private MonoMod.RuntimeDetour.Hook? assemblyLocationMonoHook; private MonoMod.RuntimeDetour.Hook? assemblyLocationMonoHook;
private MonoMod.RuntimeDetour.Hook? assemblyCodeBaseMonoHook; private MonoMod.RuntimeDetour.Hook? assemblyCodeBaseMonoHook;

View file

@ -134,6 +134,22 @@ internal record PluginManifest
[JsonProperty] [JsonProperty]
public string DownloadLinkTesting { get; init; } = null!; public string DownloadLinkTesting { get; init; } = null!;
/// <summary>
/// Gets the required Dalamud load step for this plugin to load. Takes precedence over LoadPriority.
/// Valid values are:
/// 0. During Framework.Tick, when drawing facilities are available
/// 1. During Framework.Tick
/// 2. No requirement
/// </summary>
[JsonProperty]
public int LoadRequiredState { get; init; }
/// <summary>
/// Gets a value indicating whether Dalamud must load this plugin not at the same time with other plugins and the game.
/// </summary>
[JsonProperty]
public bool LoadSync { get; init; }
/// <summary> /// <summary>
/// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority. /// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
/// </summary> /// </summary>