From 01bd9e8752e16ba3619887cf50704f647f9e6164 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Wed, 15 Jan 2020 20:20:18 -0600 Subject: [PATCH] Added actual enable/disable commit functionality --- IPA.Loader/Loader/DisabledConfig.cs | 2 + IPA.Loader/Loader/PluginManager.cs | 134 +++++++++++++++--- .../Loader/StateTransitionTransaction.cs | 18 +++ 3 files changed, 134 insertions(+), 20 deletions(-) diff --git a/IPA.Loader/Loader/DisabledConfig.cs b/IPA.Loader/Loader/DisabledConfig.cs index cbbe14e0..79013d1b 100644 --- a/IPA.Loader/Loader/DisabledConfig.cs +++ b/IPA.Loader/Loader/DisabledConfig.cs @@ -29,6 +29,8 @@ namespace IPA.Loader [UseConverter(typeof(CollectionConverter>))] public virtual HashSet DisabledModIds { get; set; } = new HashSet(); + protected internal virtual void Changed() { } + protected virtual void OnReload() { if (DisabledModIds == null || Reset) diff --git a/IPA.Loader/Loader/PluginManager.cs b/IPA.Loader/Loader/PluginManager.cs index 5f891ce3..20e373da 100644 --- a/IPA.Loader/Loader/PluginManager.cs +++ b/IPA.Loader/Loader/PluginManager.cs @@ -12,9 +12,11 @@ using IPA.Utilities; using Mono.Cecil; using UnityEngine; using Logger = IPA.Logging.Logger; -using static IPA.Loader.PluginLoader; -using IPA.Loader.Features; using System.Threading.Tasks; +#if NET4 +using TaskEx = System.Threading.Tasks.Task; +using Task = System.Threading.Tasks.Task; +#endif #if NET3 using Net3_Proxy; using Path = Net3_Proxy.Path; @@ -73,9 +75,115 @@ namespace IPA.Loader public static StateTransitionTransaction PluginStateTransaction() => new StateTransitionTransaction(AllPlugins, DisabledPlugins); + private static readonly object commitTransactionLockObject = new object(); internal static Task CommitTransaction(StateTransitionTransaction transaction) { - throw new NotImplementedException(); + lock (commitTransactionLockObject) + { + if (transaction.CurrentlyEnabled.Except(AllPlugins) + .Concat(AllPlugins.Except(transaction.CurrentlyEnabled)).Any() + || transaction.CurrentlyDisabled.Except(DisabledPlugins) + .Concat(DisabledPlugins.Except(transaction.DisabledPlugins)).Any()) + { // ensure that the transaction's base state reflects the current state, otherwise throw + throw new InvalidOperationException("Transaction no longer resembles the current state of plugins"); + } + + + var toEnable = transaction.ToEnable; + var toDisable = transaction.ToDisable; + transaction.Dispose(); + + { + // first enable the mods that need to be + void DeTree(List into, IEnumerable tree) + { + foreach (var st in tree) + if (toEnable.Contains(st) && !into.Contains(st)) + { + DeTree(into, st.Dependencies); + into.Add(st); + } + } + + var enableOrder = new List(); + DeTree(enableOrder, toEnable); + + foreach (var meta in enableOrder) + { + var executor = runtimeDisabledPlugins.FirstOrDefault(e => e.Metadata == meta); + if (executor != null) + runtimeDisabledPlugins.Remove(executor); + else + executor = PluginLoader.InitPlugin(meta, AllPlugins); + + if (executor == null) continue; // couldn't initialize, skip to next + + PluginLoader.DisabledPlugins.Remove(meta); + DisabledConfig.Instance.DisabledModIds.Remove(meta.Id ?? meta.Name); + _bsPlugins.Add(executor); + + try + { + executor.Enable(); + } + catch (Exception e) + { + Logger.loader.Error($"Error while enabling {meta.Id}:"); + Logger.loader.Error(e); + // this should still be considered enabled, hence its position + } + } + } + + Task result; + { + // then disable the mods that need to be + static DisableExecutor MakeDisableExec(PluginExecutor e) + => new DisableExecutor + { + Executor = e, + Dependents = BSMetas.Where(f => f.Metadata.Dependencies.Contains(e.Metadata)).Select(MakeDisableExec) + }; + + var disableExecs = toDisable.Select(m => BSMetas.FirstOrDefault(e => e.Metadata == m)).NonNull().ToArray(); // eagerly evaluate once + + foreach (var exec in disableExecs) + { + runtimeDisabledPlugins.Add(exec); + PluginLoader.DisabledPlugins.Add(exec.Metadata); + DisabledConfig.Instance.DisabledModIds.Add(exec.Metadata.Id ?? exec.Metadata.Name); + _bsPlugins.Remove(exec); + } + + var disableStructure = disableExecs.Select(MakeDisableExec); + + static Task Disable(DisableExecutor exec, Dictionary alreadyDisabled) + { + if (alreadyDisabled.TryGetValue(exec.Executor, out var task)) + return task; + else + { + var res = TaskEx.WhenAll(exec.Dependents.Select(d => Disable(d, alreadyDisabled))) + .ContinueWith(t => TaskEx.WhenAll(t, exec.Executor.Disable())).Unwrap(); + // The WhenAll above allows us to wait for the executor to disable, but still propagate errors + alreadyDisabled.Add(exec.Executor, res); + return res; + } + } + + var disabled = new Dictionary(); + result = TaskEx.WhenAll(disableStructure.Select(d => Disable(d, disabled))); + } + + DisabledConfig.Instance.Changed(); + return result; + } + } + + private struct DisableExecutor + { + public PluginExecutor Executor; + public IEnumerable Dependents; } // TODO: rewrite below @@ -240,6 +348,7 @@ namespace IPA.Loader /// /// a collection of all disabled plugins as public static IEnumerable DisabledPlugins => PluginLoader.DisabledPlugins; + private static readonly HashSet runtimeDisabledPlugins = new HashSet(); /// /// An invoker for the event. @@ -269,21 +378,6 @@ namespace IPA.Loader /// a collection of all enabled plugins as s public static IEnumerable AllPlugins => BSMetas.Select(p => p.Metadata); - /* - /// - /// Converts a plugin's metadata to a . - /// - /// the metadata - /// the plugin info - public static PluginInfo InfoFromMetadata(PluginMetadata meta) - { - if (IsDisabled(meta)) - return runtimeDisabled.FirstOrDefault(p => p.Metadata == meta); - else - return AllPlugins.FirstOrDefault(p => p == meta); - } - */ - /// /// An of old IPA plugins. /// @@ -323,8 +417,8 @@ namespace IPA.Loader // initialize BSIPA plugins first _bsPlugins.AddRange(PluginLoader.LoadPlugins()); - var metadataPaths = PluginsMetadata.Select(m => m.File.FullName).ToList(); - var ignoredPaths = ignoredPlugins.Select(m => m.Key.File.FullName).ToList(); + var metadataPaths = PluginLoader.PluginsMetadata.Select(m => m.File.FullName).ToList(); + var ignoredPaths = PluginLoader.ignoredPlugins.Select(m => m.Key.File.FullName).ToList(); var disabledPaths = DisabledPlugins.Select(m => m.File.FullName).ToList(); //Copy plugins to .cache diff --git a/IPA.Loader/Loader/StateTransitionTransaction.cs b/IPA.Loader/Loader/StateTransitionTransaction.cs index 918203ec..1bfa94bc 100644 --- a/IPA.Loader/Loader/StateTransitionTransaction.cs +++ b/IPA.Loader/Loader/StateTransitionTransaction.cs @@ -31,6 +31,8 @@ namespace IPA.Loader => ThrowIfDisposed() || toEnable.Concat(toDisable).Any(m => m.RuntimeOptions != RuntimeOptions.DynamicInit); + internal IEnumerable CurrentlyEnabled => currentlyEnabled; + internal IEnumerable CurrentlyDisabled => currentlyDisabled; internal IEnumerable ToEnable => toEnable; internal IEnumerable ToDisable => toDisable; @@ -199,8 +201,24 @@ namespace IPA.Loader /// /// Commits this transaction to actual state, enabling and disabling plugins as necessary. /// + /// + /// After this completes, this transaction will be disposed. + /// + /// The that is returned will error if any of the mods being disabled + /// error. It is up to the caller to handle these in a sane way, like logging them. If nothing else, do something like this: + /// + /// // get your transaction... + /// var complete = transaction.Commit(); + /// complete.ContinueWith(t => { + /// if (t.IsFaulted) + /// Logger.log.Error($"Error disabling plugins: {t.Exception}"); + /// }).Wait(); // if not Wait(), then something else to wait for completion + /// + /// + /// /// a which completes whenever all disables complete /// if this object has been disposed + /// if the plugins' state no longer matches this transaction's original state public Task Commit() => ThrowIfDisposed() ?? PluginManager.CommitTransaction(this); private void ThrowIfDisposed() => ThrowIfDisposed();