fix: prevent some deadlocks in profile management code

We can never load/unload plugins synchronously, since they might need to get back on framework thread to unload.
Also fixes an issue wherein applying might have gotten stuck if an unload threw an exception.
This commit is contained in:
goat 2023-06-20 21:55:31 +02:00
parent da8bbf5a28
commit c3fe41640e
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
7 changed files with 107 additions and 88 deletions

View file

@ -2367,12 +2367,12 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
if (inProfile) if (inProfile)
{ {
Task.Run(() => profile.AddOrUpdate(plugin.Manifest.InternalName, true)) Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true))
.ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd); .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd);
} }
else else
{ {
Task.Run(() => profile.Remove(plugin.Manifest.InternalName)) Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName))
.ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotRemove); .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotRemove);
} }
} }
@ -2391,14 +2391,17 @@ internal class PluginInstallerWindow : Window, IDisposable
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
{ {
profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, plugin.IsLoaded, false); // TODO: Work this out
Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, plugin.IsLoaded, false))
.GetAwaiter().GetResult();
foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName))) foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName)))
{ {
profile.Remove(plugin.Manifest.InternalName, false); Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName, false))
.GetAwaiter().GetResult();
} }
// TODO error handling Task.Run(profileManager.ApplyAllWantStatesAsync)
Task.Run(() => profileManager.ApplyAllWantStates()); .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_ProfileApplyFail);
} }
ImGui.SameLine(); ImGui.SameLine();
@ -2448,7 +2451,9 @@ internal class PluginInstallerWindow : Window, IDisposable
return; return;
} }
profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, false, false); // TODO: Work this out
Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, false, false))
.GetAwaiter().GetResult();
this.enableDisableStatus = OperationStatus.Complete; this.enableDisableStatus = OperationStatus.Complete;
notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success);
@ -2466,7 +2471,9 @@ internal class PluginInstallerWindow : Window, IDisposable
plugin.ReloadManifest(); plugin.ReloadManifest();
} }
profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, true, false); // TODO: Work this out
Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false))
.GetAwaiter().GetResult();
var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer)) var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer))
.ContinueWith( .ContinueWith(
@ -3282,6 +3289,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string ErrorModal_UpdaterFatal => Loc.Localize("InstallerUpdaterFatal", "Failed to update plugins.\nPlease restart your game and try again. If this error occurs again, please complain."); public static string ErrorModal_UpdaterFatal => Loc.Localize("InstallerUpdaterFatal", "Failed to update plugins.\nPlease restart your game and try again. If this error occurs again, please complain.");
public static string ErrorModal_ProfileApplyFail => Loc.Localize("InstallerProfileApplyFail", "Failed to process collections.\nPlease restart your game and try again. If this error occurs again, please complain.");
public static string ErrorModal_UpdaterFail(int failCount) => Loc.Localize("InstallerUpdaterFail", "Failed to update {0} plugins.\nPlease restart your game and try again. If this error occurs again, please complain.").Format(failCount); public static string ErrorModal_UpdaterFail(int failCount) => Loc.Localize("InstallerUpdaterFail", "Failed to update {0} plugins.\nPlease restart your game and try again. If this error occurs again, please complain.").Format(failCount);
public static string ErrorModal_UpdaterFailPartial(int successCount, int failCount) => Loc.Localize("InstallerUpdaterFailPartial", "Updated {0} plugins, failed to update {1}.\nPlease restart your game and try again. If this error occurs again, please complain.").Format(successCount, failCount); public static string ErrorModal_UpdaterFailPartial(int successCount, int failCount) => Loc.Localize("InstallerUpdaterFailPartial", "Updated {0} plugins, failed to update {1}.\nPlease restart your game and try again. If this error occurs again, please complain.").Format(successCount, failCount);

View file

@ -119,7 +119,7 @@ internal class ProfileManagerWidget
var isEnabled = profile.IsEnabled; var isEnabled = profile.IsEnabled;
if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled)) if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled))
{ {
Task.Run(() => profile.SetState(isEnabled)) Task.Run(() => profile.SetStateAsync(isEnabled))
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
} }
@ -228,9 +228,7 @@ internal class ProfileManagerWidget
if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}")) if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}"))
{ {
// TODO this sucks Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false))
profile.AddOrUpdate(plugin.Manifest.InternalName, true, false);
Task.Run(() => profman.ApplyAllWantStates())
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
} }
} }
@ -273,8 +271,12 @@ internal class ProfileManagerWidget
this.Reset(); this.Reset();
// DeleteProfile() is sync, it doesn't apply and we are modifying the plugins collection. Will throw below when iterating // DeleteProfile() is sync, it doesn't apply and we are modifying the plugins collection. Will throw below when iterating
profman.DeleteProfile(profile); // TODO: DeleteProfileAsync should probably apply as well
Task.Run(() => profman.ApplyAllWantStates()) Task.Run(async () =>
{
await profman.DeleteProfileAsync(profile);
await profman.ApplyAllWantStatesAsync();
})
.ContinueWith(t => .ContinueWith(t =>
{ {
this.installer.DisplayErrorContinuation(t, Locs.ErrorCouldNotChangeState); this.installer.DisplayErrorContinuation(t, Locs.ErrorCouldNotChangeState);
@ -300,7 +302,7 @@ internal class ProfileManagerWidget
var isEnabled = profile.IsEnabled; var isEnabled = profile.IsEnabled;
if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled)) if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled))
{ {
Task.Run(() => profile.SetState(isEnabled)) Task.Run(() => profile.SetStateAsync(isEnabled))
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
} }
@ -391,7 +393,7 @@ internal class ProfileManagerWidget
var enabled = plugin.IsEnabled; var enabled = plugin.IsEnabled;
if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled)) if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled))
{ {
Task.Run(() => profile.AddOrUpdate(plugin.InternalName, enabled)) Task.Run(() => profile.AddOrUpdateAsync(plugin.InternalName, enabled))
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
} }
@ -411,9 +413,8 @@ internal class ProfileManagerWidget
if (wantRemovePluginInternalName != null) if (wantRemovePluginInternalName != null)
{ {
// TODO: handle error // TODO: handle error
profile.Remove(wantRemovePluginInternalName, false); Task.Run(() => profile.RemoveAsync(wantRemovePluginInternalName, false))
Task.Run(() => profman.ApplyAllWantStates()) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove);
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove);
} }
if (!didAny) if (!didAny)

View file

@ -262,6 +262,7 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <summary> /// <summary>
/// Get a disposable that will lock plugin lists while it is not disposed. /// Get a disposable that will lock plugin lists while it is not disposed.
/// You must NEVER use this in async code.
/// </summary> /// </summary>
/// <returns>The aforementioned disposable.</returns> /// <returns>The aforementioned disposable.</returns>
public IDisposable GetSyncScope() => new ScopedSyncRoot(this.pluginListLock); public IDisposable GetSyncScope() => new ScopedSyncRoot(this.pluginListLock);
@ -1290,28 +1291,28 @@ internal partial class PluginManager : IDisposable, IServiceType
{ {
// We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled. // We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled.
Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", probablyInternalNameForThisPurpose); Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", probablyInternalNameForThisPurpose);
this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false);
loadPlugin = false; loadPlugin = false;
} }
else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot) else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot)
{ {
// We wanted this plugin, and StartOnBoot is on. That means we actually do want it. // We wanted this plugin, and StartOnBoot is on. That means we actually do want it.
Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", probablyInternalNameForThisPurpose); Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", probablyInternalNameForThisPurpose);
this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, true, false); await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, true, false);
loadPlugin = !doNotLoad; loadPlugin = !doNotLoad;
} }
else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot) else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot)
{ {
// We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore. // We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore.
Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose); Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose);
this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false);
loadPlugin = false; loadPlugin = false;
} }
else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot) else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot)
{ {
// We didn't want this plugin, and StartOnBoot is off. We don't want it. // We didn't want this plugin, and StartOnBoot is off. We don't want it.
Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose); Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose);
this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false);
loadPlugin = false; loadPlugin = false;
} }
@ -1328,7 +1329,7 @@ internal partial class PluginManager : IDisposable, IServiceType
#pragma warning restore CS0618 #pragma warning restore CS0618
// Need to do this here, so plugins that don't load are still added to the default profile // Need to do this here, so plugins that don't load are still added to the default profile
var wantToLoad = this.profileManager.GetWantState(plugin.Manifest.InternalName, defaultState); var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.InternalName, defaultState);
if (loadPlugin) if (loadPlugin)
{ {

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
@ -115,7 +116,8 @@ internal class Profile
/// <param name="enabled">Whether or not the profile is enabled.</param> /// <param name="enabled">Whether or not the profile is enabled.</param>
/// <param name="apply">Whether or not the current state should immediately be applied.</param> /// <param name="apply">Whether or not the current state should immediately be applied.</param>
/// <exception cref="InvalidOperationException">Thrown when an untoggleable profile is toggled.</exception> /// <exception cref="InvalidOperationException">Thrown when an untoggleable profile is toggled.</exception>
public void SetState(bool enabled, bool apply = true) /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task SetStateAsync(bool enabled, bool apply = true)
{ {
if (this.IsDefaultProfile) if (this.IsDefaultProfile)
throw new InvalidOperationException("Cannot set state of default profile"); throw new InvalidOperationException("Cannot set state of default profile");
@ -127,7 +129,7 @@ internal class Profile
Service<DalamudConfiguration>.Get().QueueSave(); Service<DalamudConfiguration>.Get().QueueSave();
if (apply) if (apply)
this.manager.ApplyAllWantStates(); await this.manager.ApplyAllWantStatesAsync();
} }
/// <summary> /// <summary>
@ -137,10 +139,11 @@ internal class Profile
/// <returns>Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled.</returns> /// <returns>Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled.</returns>
public bool? WantsPlugin(string internalName) public bool? WantsPlugin(string internalName)
{ {
using var lockScope = this.manager.GetSyncScope(); lock (this)
{
var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
return entry?.IsEnabled; return entry?.IsEnabled;
}
} }
/// <summary> /// <summary>
@ -150,36 +153,38 @@ internal class Profile
/// <param name="internalName">The internal name of the plugin.</param> /// <param name="internalName">The internal name of the plugin.</param>
/// <param name="state">Whether or not the plugin should be enabled.</param> /// <param name="state">Whether or not the plugin should be enabled.</param>
/// <param name="apply">Whether or not the current state should immediately be applied.</param> /// <param name="apply">Whether or not the current state should immediately be applied.</param>
public void AddOrUpdate(string internalName, bool state, bool apply = true) /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task AddOrUpdateAsync(string internalName, bool state, bool apply = true)
{ {
using var lockScope = this.manager.GetSyncScope();
Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()"); Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()");
var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); lock (this)
if (existing != null)
{ {
existing.IsEnabled = state; var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
} if (existing != null)
else
{
this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin
{ {
InternalName = internalName, existing.IsEnabled = state;
IsEnabled = state, }
}); else
{
this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin
{
InternalName = internalName,
IsEnabled = state,
});
}
} }
// We need to remove this plugin from the default profile, if it declares it. // We need to remove this plugin from the default profile, if it declares it.
if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(internalName) != null) if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(internalName) != null)
{ {
this.manager.DefaultProfile.Remove(internalName, false); await this.manager.DefaultProfile.RemoveAsync(internalName, false);
} }
Service<DalamudConfiguration>.Get().QueueSave(); Service<DalamudConfiguration>.Get().QueueSave();
if (apply) if (apply)
this.manager.ApplyAllWantStates(); await this.manager.ApplyAllWantStatesAsync();
} }
/// <summary> /// <summary>
@ -188,23 +193,26 @@ internal class Profile
/// </summary> /// </summary>
/// <param name="internalName">The internal name of the plugin.</param> /// <param name="internalName">The internal name of the plugin.</param>
/// <param name="apply">Whether or not the current state should immediately be applied.</param> /// <param name="apply">Whether or not the current state should immediately be applied.</param>
public void Remove(string internalName, bool apply = true) /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task RemoveAsync(string internalName, bool apply = true)
{ {
using var lockScope = this.manager.GetSyncScope(); ProfileModelV1.ProfileModelV1Plugin entry;
lock (this)
var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); {
if (entry == null) entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\""); if (entry == null)
throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\"");
if (!this.modelV1.Plugins.Remove(entry)) if (!this.modelV1.Plugins.Remove(entry))
throw new Exception("Couldn't remove plugin from model collection"); throw new Exception("Couldn't remove plugin from model collection");
}
// We need to add this plugin back to the default profile, if we were the last profile to have it. // We need to add this plugin back to the default profile, if we were the last profile to have it.
if (!this.manager.IsInAnyProfile(internalName)) if (!this.manager.IsInAnyProfile(internalName))
{ {
if (!this.IsDefaultProfile) if (!this.IsDefaultProfile)
{ {
this.manager.DefaultProfile.AddOrUpdate(internalName, entry.IsEnabled, false); await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, entry.IsEnabled, false);
} }
else else
{ {
@ -215,6 +223,6 @@ internal class Profile
Service<DalamudConfiguration>.Get().QueueSave(); Service<DalamudConfiguration>.Get().QueueSave();
if (apply) if (apply)
this.manager.ApplyAllWantStates(); await this.manager.ApplyAllWantStatesAsync();
} }
} }

View file

@ -96,14 +96,14 @@ internal class ProfileCommandHandler : IServiceType, IDisposable
{ {
case ProfileOp.Enable: case ProfileOp.Enable:
if (!profile.IsEnabled) if (!profile.IsEnabled)
profile.SetState(true, false); Task.Run(() => profile.SetStateAsync(true, false)).GetAwaiter().GetResult();
break; break;
case ProfileOp.Disable: case ProfileOp.Disable:
if (profile.IsEnabled) if (profile.IsEnabled)
profile.SetState(false, false); Task.Run(() => profile.SetStateAsync(false, false)).GetAwaiter().GetResult();
break; break;
case ProfileOp.Toggle: case ProfileOp.Toggle:
profile.SetState(!profile.IsEnabled, false); Task.Run(() => profile.SetStateAsync(!profile.IsEnabled, false)).GetAwaiter().GetResult();
break; break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
@ -118,7 +118,7 @@ internal class ProfileCommandHandler : IServiceType, IDisposable
this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name)); this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name));
} }
Task.Run(() => this.profileManager.ApplyAllWantStates()).ContinueWith(t => Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t =>
{ {
if (!t.IsCompletedSuccessfully && t.Exception != null) if (!t.IsCompletedSuccessfully && t.Exception != null)
{ {

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -60,12 +59,6 @@ internal class ProfileManager : IServiceType
/// </summary> /// </summary>
public bool IsBusy => this.isBusy; public bool IsBusy => this.isBusy;
/// <summary>
/// Get a disposable that will lock the profile list while it is not disposed.
/// </summary>
/// <returns>The aforementioned disposable.</returns>
public ScopedSyncRoot GetSyncScope() => new ScopedSyncRoot(this.profiles);
/// <summary> /// <summary>
/// Check if any enabled profile wants a specific plugin enabled. /// Check if any enabled profile wants a specific plugin enabled.
/// </summary> /// </summary>
@ -73,13 +66,13 @@ internal class ProfileManager : IServiceType
/// <param name="defaultState">The state the plugin shall be in, if it needs to be added.</param> /// <param name="defaultState">The state the plugin shall be in, if it needs to be added.</param>
/// <param name="addIfNotDeclared">Whether or not the plugin should be added to the default preset, if it's not present in any preset.</param> /// <param name="addIfNotDeclared">Whether or not the plugin should be added to the default preset, if it's not present in any preset.</param>
/// <returns>Whether or not the plugin shall be enabled.</returns> /// <returns>Whether or not the plugin shall be enabled.</returns>
public bool GetWantState(string internalName, bool defaultState, bool addIfNotDeclared = true) public async Task<bool> GetWantStateAsync(string internalName, bool defaultState, bool addIfNotDeclared = true)
{ {
var want = false;
var wasInAnyProfile = false;
lock (this.profiles) lock (this.profiles)
{ {
var want = false;
var wasInAnyProfile = false;
foreach (var profile in this.profiles) foreach (var profile in this.profiles)
{ {
var state = profile.WantsPlugin(internalName); var state = profile.WantsPlugin(internalName);
@ -89,16 +82,17 @@ internal class ProfileManager : IServiceType
wasInAnyProfile = true; wasInAnyProfile = true;
} }
} }
if (!wasInAnyProfile && addIfNotDeclared)
{
Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState);
this.DefaultProfile.AddOrUpdate(internalName, defaultState, false);
return defaultState;
}
return want;
} }
if (!wasInAnyProfile && addIfNotDeclared)
{
Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState);
await this.DefaultProfile.AddOrUpdateAsync(internalName, defaultState, false);
return defaultState;
}
return want;
} }
/// <summary> /// <summary>
@ -186,20 +180,24 @@ internal class ProfileManager : IServiceType
/// Go through all profiles and plugins, and enable/disable plugins they want active. /// Go through all profiles and plugins, and enable/disable plugins they want active.
/// This will block until all plugins have been loaded/reloaded. /// This will block until all plugins have been loaded/reloaded.
/// </summary> /// </summary>
public void ApplyAllWantStates() /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task ApplyAllWantStatesAsync()
{ {
var pm = Service<PluginManager>.Get(); if (this.isBusy)
using var managerLock = pm.GetSyncScope(); throw new Exception("Already busy, this must not run in parallel. Check before starting another apply!");
using var profilesLock = this.GetSyncScope();
this.isBusy = true; this.isBusy = true;
Log.Information("Getting want states..."); Log.Information("Getting want states...");
var wantActive = this.profiles List<string> wantActive;
lock (this.profiles)
{
wantActive = this.profiles
.Where(x => x.IsEnabled) .Where(x => x.IsEnabled)
.SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled) .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled)
.Select(plugin => plugin.InternalName)) .Select(plugin => plugin.InternalName))
.Distinct().ToList(); .Distinct().ToList();
}
foreach (var internalName in wantActive) foreach (var internalName in wantActive)
{ {
@ -210,6 +208,7 @@ internal class ProfileManager : IServiceType
var tasks = new List<Task>(); var tasks = new List<Task>();
var pm = Service<PluginManager>.Get();
foreach (var installedPlugin in pm.InstalledPlugins) foreach (var installedPlugin in pm.InstalledPlugins)
{ {
var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName); var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName);
@ -237,7 +236,7 @@ internal class ProfileManager : IServiceType
// This is probably not ideal... Might need to rethink the error handling strategy for this. // This is probably not ideal... Might need to rethink the error handling strategy for this.
try try
{ {
Task.WaitAll(tasks.ToArray()); await Task.WhenAll(tasks.ToArray());
} }
catch (Exception e) catch (Exception e)
{ {
@ -255,12 +254,13 @@ internal class ProfileManager : IServiceType
/// You should definitely apply states after this. It doesn't do it for you. /// You should definitely apply states after this. It doesn't do it for you.
/// </remarks> /// </remarks>
/// <param name="profile">The profile to delete.</param> /// <param name="profile">The profile to delete.</param>
public void DeleteProfile(Profile profile) /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task DeleteProfileAsync(Profile profile)
{ {
// We need to remove all plugins from the profile first, so that they are re-added to the default profile if needed // We need to remove all plugins from the profile first, so that they are re-added to the default profile if needed
foreach (var plugin in profile.Plugins.ToArray()) foreach (var plugin in profile.Plugins.ToArray())
{ {
profile.Remove(plugin.InternalName, false); await profile.RemoveAsync(plugin.InternalName, false);
} }
if (!this.config.SavedProfiles!.Remove(profile.Model)) if (!this.config.SavedProfiles!.Remove(profile.Model))

View file

@ -213,7 +213,7 @@ internal class LocalPlugin : IDisposable
/// INCLUDES the default profile. /// INCLUDES the default profile.
/// </summary> /// </summary>
public bool IsWantedByAnyProfile => public bool IsWantedByAnyProfile =>
Service<ProfileManager>.Get().GetWantState(this.Manifest.InternalName, false, false); Service<ProfileManager>.Get().GetWantStateAsync(this.Manifest.InternalName, false, false).GetAwaiter().GetResult();
/// <summary> /// <summary>
/// Gets a value indicating whether this plugin's API level is out of date. /// Gets a value indicating whether this plugin's API level is out of date.