Merge pull request #1673 from Soreepeong/fix/igameconfig-racecon

IGameConfig: fix load-time race condition
This commit is contained in:
goat 2024-02-25 19:00:00 +01:00 committed by GitHub
commit 6116508c57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 130 additions and 33 deletions

View file

@ -1,4 +1,6 @@
using Dalamud.Hooking; using System.Threading.Tasks;
using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
@ -15,6 +17,11 @@ namespace Dalamud.Game.Config;
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
{ {
private readonly TaskCompletionSource tcsInitialization = new();
private readonly TaskCompletionSource<GameConfigSection> tcsSystem = new();
private readonly TaskCompletionSource<GameConfigSection> tcsUiConfig = new();
private readonly TaskCompletionSource<GameConfigSection> tcsUiControl = new();
private readonly GameConfigAddressResolver address = new(); private readonly GameConfigAddressResolver address = new();
private Hook<ConfigChangeDelegate>? configChangeHook; private Hook<ConfigChangeDelegate>? configChangeHook;
@ -23,16 +30,32 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
{ {
framework.RunOnTick(() => framework.RunOnTick(() =>
{ {
Log.Verbose("[GameConfig] Initializing"); try
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); {
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig; Log.Verbose("[GameConfig] Initializing");
this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase); var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig); var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig); this.tcsSystem.SetResult(new("System", framework, &commonConfig->ConfigBase));
this.tcsUiConfig.SetResult(new("UiConfig", framework, &commonConfig->UiConfig));
this.address.Setup(sigScanner); this.tcsUiControl.SetResult(
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged); new(
this.configChangeHook.Enable(); "UiControl",
framework,
() => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode
? &commonConfig->UiControlGamepadConfig
: &commonConfig->UiControlConfig));
this.address.Setup(sigScanner);
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(
this.address.ConfigChangeAddress,
this.OnConfigChanged);
this.configChangeHook.Enable();
this.tcsInitialization.SetResult();
}
catch (Exception ex)
{
this.tcsInitialization.SetExceptionIfIncomplete(ex);
}
}); });
} }
@ -58,14 +81,19 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
public event EventHandler<ConfigChangeEvent>? UiControlChanged; public event EventHandler<ConfigChangeEvent>? UiControlChanged;
#pragma warning restore 67 #pragma warning restore 67
/// <inheritdoc/> /// <summary>
public GameConfigSection System { get; private set; } /// Gets a task representing the initialization state of this class.
/// </summary>
public Task InitializationTask => this.tcsInitialization.Task;
/// <inheritdoc/> /// <inheritdoc/>
public GameConfigSection UiConfig { get; private set; } public GameConfigSection System => this.tcsSystem.Task.Result;
/// <inheritdoc/> /// <inheritdoc/>
public GameConfigSection UiControl { get; private set; } public GameConfigSection UiConfig => this.tcsUiConfig.Task.Result;
/// <inheritdoc/>
public GameConfigSection UiControl => this.tcsUiControl.Task.Result;
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value); public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value);
@ -169,6 +197,11 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
/// <inheritdoc/> /// <inheritdoc/>
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
var ode = new ObjectDisposedException(nameof(GameConfig));
this.tcsInitialization.SetExceptionIfIncomplete(ode);
this.tcsSystem.SetExceptionIfIncomplete(ode);
this.tcsUiConfig.SetExceptionIfIncomplete(ode);
this.tcsUiControl.SetExceptionIfIncomplete(ode);
this.configChangeHook?.Disable(); this.configChangeHook?.Disable();
this.configChangeHook?.Dispose(); this.configChangeHook?.Dispose();
} }
@ -220,15 +253,24 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly GameConfig gameConfigService = Service<GameConfig>.Get(); private readonly GameConfig gameConfigService = Service<GameConfig>.Get();
private readonly Task initializationTask;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GameConfigPluginScoped"/> class. /// Initializes a new instance of the <see cref="GameConfigPluginScoped"/> class.
/// </summary> /// </summary>
internal GameConfigPluginScoped() internal GameConfigPluginScoped()
{ {
this.gameConfigService.Changed += this.ConfigChangedForward; this.gameConfigService.Changed += this.ConfigChangedForward;
this.gameConfigService.System.Changed += this.SystemConfigChangedForward; this.initializationTask = this.gameConfigService.InitializationTask.ContinueWith(
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward; r =>
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward; {
if (!r.IsCompletedSuccessfully)
return r;
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
return Task.CompletedTask;
}).Unwrap();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -256,9 +298,15 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
public void Dispose() public void Dispose()
{ {
this.gameConfigService.Changed -= this.ConfigChangedForward; this.gameConfigService.Changed -= this.ConfigChangedForward;
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward; this.initializationTask.ContinueWith(
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward; r =>
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward; {
if (!r.IsCompletedSuccessfully)
return;
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
});
this.Changed = null; this.Changed = null;
this.SystemChanged = null; this.SystemChanged = null;

View file

@ -1,14 +1,23 @@
using System; using System.Diagnostics;
using System.Diagnostics; using System.Threading.Tasks;
using Dalamud.Game.Config; using Dalamud.Game.Config;
using FFXIVClientStructs.FFXIV.Common.Configuration; using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Plugin.Services; namespace Dalamud.Plugin.Services;
/// <summary> /// <summary>
/// This class represents the game's configuration. /// This class represents the game's configuration.
/// </summary> /// </summary>
/// <remarks>
/// Accessing <see cref="GameConfigSection"/>-typed properties such as <see cref="System"/>, directly or indirectly
/// via <see cref="TryGet(Game.Config.SystemConfigOption,out bool)"/>,
/// <see cref="Set(Game.Config.SystemConfigOption,bool)"/>, or alike will block, if the game is not done loading.<br />
/// Therefore, avoid accessing configuration from your plugin constructor, especially if your plugin sets
/// <see cref="PluginManifest.LoadRequiredState"/> to <c>2</c> and <see cref="PluginManifest.LoadSync"/> to <c>true</c>.
/// If property access from the plugin constructor is desired, do the value retrieval asynchronously via
/// <see cref="IFramework.RunOnFrameworkThread{T}(Func{T})"/>; do not wait for the result right away.
/// </remarks>
public interface IGameConfig public interface IGameConfig
{ {
/// <summary> /// <summary>

View file

@ -194,12 +194,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
try try
{ {
await using var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write); await using (var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write))
await url.DownloadAsync( {
this.httpClient.SharedHttpClient, await url.DownloadAsync(
tempPathStream, this.httpClient.SharedHttpClient,
this.cancellationTokenSource.Token); tempPathStream,
tempPathStream.Dispose(); this.cancellationTokenSource.Token);
}
for (var j = RenameAttemptCount; ; j--) for (var j = RenameAttemptCount; ; j--)
{ {
try try
@ -265,7 +267,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
/// <inheritdoc/> /// <inheritdoc/>
[Pure] [Pure]
public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) => public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) =>
ExtractResult(this.GetDalamudTextureWrapAsync(asset)); this.GetDalamudTextureWrapAsync(asset).Result;
/// <inheritdoc/> /// <inheritdoc/>
[Pure] [Pure]
@ -332,8 +334,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
} }
} }
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
private Task<TOut> TransformImmediate<TIn, TOut>(Task<TIn> task, Func<TIn, TOut> transformer) private Task<TOut> TransformImmediate<TIn, TOut>(Task<TIn> task, Func<TIn, TOut> transformer)
{ {
if (task.IsCompletedSuccessfully) if (task.IsCompletedSuccessfully)

View file

@ -10,6 +10,7 @@ using System.Reflection.Emit;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Data; using Dalamud.Data;
@ -697,6 +698,45 @@ public static class Util
Marshal.ThrowExceptionForHR(hr.Value); Marshal.ThrowExceptionForHR(hr.Value);
} }
/// <summary>
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
/// </summary>
/// <param name="t">The task.</param>
/// <param name="ex">The exception to set.</param>
internal static void SetExceptionIfIncomplete(this TaskCompletionSource t, Exception ex)
{
if (t.Task.IsCompleted)
return;
try
{
t.SetException(ex);
}
catch
{
// ignore
}
}
/// <summary>
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
/// </summary>
/// <typeparam name="T">The type of the result.</typeparam>
/// <param name="t">The task.</param>
/// <param name="ex">The exception to set.</param>
internal static void SetExceptionIfIncomplete<T>(this TaskCompletionSource<T> t, Exception ex)
{
if (t.Task.IsCompleted)
return;
try
{
t.SetException(ex);
}
catch
{
// ignore
}
}
/// <summary> /// <summary>
/// Print formatted GameObject Information to ImGui. /// Print formatted GameObject Information to ImGui.
/// </summary> /// </summary>