diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs index 06847994..2a3afc09 100644 --- a/IPA.Loader/Config/SelfConfig.cs +++ b/IPA.Loader/Config/SelfConfig.cs @@ -29,7 +29,7 @@ namespace IPA.Config protected internal virtual void OnReload() { if (Regenerate) - CopyFrom(new SelfConfig()); + CopyFrom(new SelfConfig { Regenerate = false }); StandardLogger.Configure(); } @@ -75,7 +75,7 @@ namespace IPA.Config // END: section ignore - public virtual bool Regenerate { get; set; } = false; + public virtual bool Regenerate { get; set; } = true; public class Updates_ { diff --git a/IPA.Loader/Config/Stores/GeneratedStore.cs b/IPA.Loader/Config/Stores/GeneratedStore.cs index 255d5b13..daae8b8a 100644 --- a/IPA.Loader/Config/Stores/GeneratedStore.cs +++ b/IPA.Loader/Config/Stores/GeneratedStore.cs @@ -77,7 +77,12 @@ namespace IPA.Config.Stores /// another object of its type easily, and more importantly, as only one change. Its body will be executed after the values have been copied. /// /// - /// TODO: describe details of generated stores + /// Similarly, can declare a public or protected, + /// method ChangeTransaction() returning , which may be called to get an object representing a transactional + /// change. This may be used to change a lot of properties at once without triggering a save multiple times. Ideally, this is used in a + /// block or declaration. The returned from your implementation will have its + /// called after Changed() is called, but before the write lock is released. + /// Unless you have a very good reason to use the nested , avoid it. /// /// /// the type to wrap @@ -107,6 +112,7 @@ namespace IPA.Config.Stores void OnReload(); void Changed(); + IDisposable ChangeTransaction(); Value Serialize(); void Deserialize(Value val); @@ -119,6 +125,7 @@ namespace IPA.Config.Stores internal class Impl : IConfigStore { private readonly IGeneratedStore generated; + private bool inChangeTransaction = false; internal static ConstructorInfo Ctor = typeof(Impl).GetConstructor(new[] { typeof(IGeneratedStore) }); public Impl(IGeneratedStore store) => generated = store; @@ -142,11 +149,19 @@ namespace IPA.Config.Stores internal static MethodInfo ImplTakeReadMethod = typeof(Impl).GetMethod(nameof(ImplTakeRead)); public static void ImplTakeRead(IGeneratedStore s) => FindImpl(s).TakeRead(); - public void TakeRead() => WriteSyncObject.EnterReadLock(); + public void TakeRead() + { + if (!WriteSyncObject.IsWriteLockHeld) + WriteSyncObject.EnterReadLock(); + } internal static MethodInfo ImplReleaseReadMethod = typeof(Impl).GetMethod(nameof(ImplReleaseRead)); public static void ImplReleaseRead(IGeneratedStore s) => FindImpl(s).ReleaseRead(); - public void ReleaseRead() => WriteSyncObject.ExitReadLock(); + public void ReleaseRead() + { + if (!WriteSyncObject.IsWriteLockHeld) + WriteSyncObject.ExitReadLock(); + } internal static MethodInfo ImplTakeWriteMethod = typeof(Impl).GetMethod(nameof(ImplTakeWrite)); public static void ImplTakeWrite(IGeneratedStore s) => FindImpl(s).TakeWrite(); @@ -156,6 +171,44 @@ namespace IPA.Config.Stores public static void ImplReleaseWrite(IGeneratedStore s) => FindImpl(s).ReleaseWrite(); public void ReleaseWrite() => WriteSyncObject.ExitWriteLock(); + internal static MethodInfo ImplChangeTransactionMethod = typeof(Impl).GetMethod(nameof(ImplChangeTransaction)); + public static IDisposable ImplChangeTransaction(IGeneratedStore s, IDisposable nest) => FindImpl(s).ChangeTransaction(nest); + // TODO: use some fixed pool of these, because their lifetimes are hella short + public IDisposable ChangeTransaction(IDisposable nest, bool takeWrite = true) + => new ChangeTransactionObj(this, !inChangeTransaction, nest, takeWrite && !WriteSyncObject.IsWriteLockHeld); + + private sealed class ChangeTransactionObj : IDisposable + { + private readonly Impl impl; + private readonly bool owns; + private readonly bool ownsWrite; + private readonly IDisposable nested; + + public ChangeTransactionObj(Impl impl, bool owning, IDisposable nest, bool takeWrite) + { + this.impl = impl; + nested = nest; + if (owns = owning) + impl.inChangeTransaction = true; + if (ownsWrite = takeWrite) + impl.TakeWrite(); + } + + public void Dispose() + { + if (owns) + { + impl.inChangeTransaction = false; + impl.InvokeChanged(); + } + nested?.Dispose(); + if (ownsWrite) + impl.ReleaseWrite(); + GC.SuppressFinalize(this); + } + + ~ChangeTransactionObj() => Dispose(); + } public static Impl FindImpl(IGeneratedStore store) { @@ -173,9 +226,8 @@ namespace IPA.Config.Stores Logger.config.Debug($"Read {values}"); generated.Deserialize(values); - ReleaseWrite(); + using var transaction = generated.ChangeTransaction(); generated.OnReload(); - TakeWrite(); // must take again for runtime to be happy (which is unfortunate) } internal static MethodInfo ImplWriteToMethod = typeof(Impl).GetMethod(nameof(ImplWriteTo)); @@ -290,6 +342,7 @@ namespace IPA.Config.Stores internal delegate IConfigStore GeneratedStoreCreator(IGeneratedStore parent); + private static bool IsMethodInvalid(MethodInfo m, Type ret) => !m.IsVirtual || m.ReturnType != ret; private static (GeneratedStoreCreator ctor, Type type) MakeCreator(Type type) { // note that this does not and should not use converters by default for everything if (!type.IsClass) throw new ArgumentException("Config type is not a class"); @@ -299,12 +352,18 @@ namespace IPA.Config.Stores throw new ArgumentException("Config type does not have a public parameterless constructor"); #region Parse base object structure - var baseChanged = type.GetMethod("Changed", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, Array.Empty()); - if (baseChanged != null && !baseChanged.IsVirtual) baseChanged = null; - var baseOnReload = type.GetMethod("OnReload", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, Array.Empty()); - if (baseOnReload != null && !baseOnReload.IsVirtual) baseOnReload = null; - var baseCopyFrom = type.GetMethod("CopyFrom", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { type }, Array.Empty()); - if (baseCopyFrom != null && !baseCopyFrom.IsVirtual) baseCopyFrom = null; + const BindingFlags overrideMemberFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + var baseChanged = type.GetMethod("Changed", overrideMemberFlags, null, Type.EmptyTypes, Array.Empty()); + if (baseChanged != null && IsMethodInvalid(baseChanged, typeof(void))) baseChanged = null; + + var baseOnReload = type.GetMethod("OnReload", overrideMemberFlags, null, Type.EmptyTypes, Array.Empty()); + if (baseOnReload != null && IsMethodInvalid(baseOnReload, typeof(void))) baseOnReload = null; + + var baseCopyFrom = type.GetMethod("CopyFrom", overrideMemberFlags, null, new[] { type }, Array.Empty()); + if (baseCopyFrom != null && IsMethodInvalid(baseCopyFrom, typeof(void))) baseCopyFrom = null; + + var baseChangeTransaction = type.GetMethod("ChangeTransaction", overrideMemberFlags, null, Type.EmptyTypes, Array.Empty()); + if (baseChangeTransaction != null && IsMethodInvalid(baseChangeTransaction, typeof(IDisposable))) baseChangeTransaction = null; var structure = new List(); @@ -529,6 +588,7 @@ namespace IPA.Config.Stores var IGeneratedStore_Deserialize = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.Deserialize)); var IGeneratedStore_OnReload = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.OnReload)); var IGeneratedStore_Changed = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.Changed)); + var IGeneratedStore_ChangeTransaction = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.ChangeTransaction)); #region IGeneratedStore.OnReload var onReload = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.OnReload)}", virtualMemberMethod, null, Type.EmptyTypes); @@ -691,56 +751,6 @@ namespace IPA.Config.Stores #endregion #endregion - #region IGeneratedStore - var IGeneratedStore_T_t = typeof(IGeneratedStore<>).MakeGenericType(type); - typeBuilder.AddInterfaceImplementation(IGeneratedStore_T_t); - - var IGeneratedStore_T_CopyFrom = IGeneratedStore_T_t.GetMethod(nameof(IGeneratedStore.CopyFrom)); - - #region IGeneratedStore.CopyFrom - var copyFrom = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.CopyFrom)}", virtualMemberMethod, null, new[] { type, typeof(bool) }); - typeBuilder.DefineMethodOverride(copyFrom, IGeneratedStore_T_CopyFrom); - - { - var il = copyFrom.GetILGenerator(); - - var startLock = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Brfalse, startLock); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, Impl.ImplTakeWriteMethod); // take the write lock - il.MarkLabel(startLock); - - var GetLocal = MakeGetLocal(il); - - foreach (var member in structure) - { - il.BeginExceptionBlock(); - - EmitStore(il, member, il => - { - EmitLoad(il, member, il => il.Emit(OpCodes.Ldarg_1)); - EmitCorrectMember(il, member, false, false, GetLocal); - }); - - il.BeginCatchBlock(typeof(Exception)); - - EmitWarnException(il, $"Error while copying from member {member.Name}"); - - il.EndExceptionBlock(); - } - - var endLock = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Brfalse, endLock); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, Impl.ImplReleaseWriteMethod); // release write lock - il.MarkLabel(endLock); - il.Emit(OpCodes.Ret); - } - #endregion - #endregion - #region IConfigStore typeBuilder.AddInterfaceImplementation(typeof(IConfigStore)); @@ -815,39 +825,104 @@ namespace IPA.Config.Stores "<>Changed", virtualMemberMethod, null, Type.EmptyTypes); + typeBuilder.DefineMethodOverride(coreChanged, IGeneratedStore_Changed); + if (baseChanged != null) + typeBuilder.DefineMethodOverride(coreChanged, baseChanged); { var il = coreChanged.GetILGenerator(); - il.Emit(OpCodes.Ldarg_0); + if (baseChanged != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, baseChanged); // call base + } + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, Impl.ImplSignalChangedMethod); il.Emit(OpCodes.Ret); // simply call our impl's SignalChanged method and return - } + } + #endregion + + #region ChangeTransaction + var coreChangeTransaction = typeBuilder.DefineMethod( + "<>ChangeTransaction", + virtualMemberMethod, + typeof(IDisposable), Type.EmptyTypes); + typeBuilder.DefineMethodOverride(coreChangeTransaction, IGeneratedStore_ChangeTransaction); + if (baseChangeTransaction != null) + typeBuilder.DefineMethodOverride(coreChangeTransaction, baseChangeTransaction); + + { + var il = coreChangeTransaction.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + if (baseChangeTransaction != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, baseChangeTransaction); + } + else + il.Emit(OpCodes.Ldnull); + + il.Emit(OpCodes.Tailcall); + il.Emit(OpCodes.Call, Impl.ImplChangeTransactionMethod); + il.Emit(OpCodes.Ret); + } + #endregion + + #region IGeneratedStore + var IGeneratedStore_T_t = typeof(IGeneratedStore<>).MakeGenericType(type); + typeBuilder.AddInterfaceImplementation(IGeneratedStore_T_t); + + var IGeneratedStore_T_CopyFrom = IGeneratedStore_T_t.GetMethod(nameof(IGeneratedStore.CopyFrom)); + + #region IGeneratedStore.CopyFrom + var copyFrom = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.CopyFrom)}", virtualMemberMethod, null, new[] { type, typeof(bool) }); + typeBuilder.DefineMethodOverride(copyFrom, IGeneratedStore_T_CopyFrom); + + { + var il = copyFrom.GetILGenerator(); + + var transactionLocal = il.DeclareLocal(IDisposable_t); + + var startLock = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Brfalse, startLock); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, coreChangeTransaction); // take the write lock + il.Emit(OpCodes.Stloc, transactionLocal); + il.MarkLabel(startLock); - if (baseChanged != null) { - var changedMethod = typeBuilder.DefineMethod( // copy to override baseChanged - baseChanged.Name, - virtualMemberMethod, - null, Type.EmptyTypes); - typeBuilder.DefineMethodOverride(changedMethod, baseChanged); + var GetLocal = MakeGetLocal(il); + foreach (var member in structure) { - var il = changedMethod.GetILGenerator(); + il.BeginExceptionBlock(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, baseChanged); // call base + EmitStore(il, member, il => + { + EmitLoad(il, member, il => il.Emit(OpCodes.Ldarg_1)); + EmitCorrectMember(il, member, false, false, GetLocal); + }); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Tailcall); - il.Emit(OpCodes.Call, coreChanged); // call back to the core change method + il.BeginCatchBlock(typeof(Exception)); - il.Emit(OpCodes.Ret); - } + EmitWarnException(il, $"Error while copying from member {member.Name}"); - coreChanged = changedMethod; // switch to calling this version instead of just the default + il.EndExceptionBlock(); + } + + var endLock = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Brfalse, endLock); + il.Emit(OpCodes.Ldloc, transactionLocal); + il.Emit(OpCodes.Callvirt, IDisposable_Dispose); + il.MarkLabel(endLock); + il.Emit(OpCodes.Ret); } - - typeBuilder.DefineMethodOverride(coreChanged, IGeneratedStore_Changed); + #endregion #endregion #region base.CopyFrom @@ -862,20 +937,20 @@ namespace IPA.Config.Stores { var il = pubCopyFrom.GetILGenerator(); - // TODO: use transactional changes + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, coreChangeTransaction); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Call, copyFrom); // call internal il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Call, baseCopyFrom); // call base - il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Tailcall); - il.Emit(OpCodes.Call, coreChanged); // call changed + il.Emit(OpCodes.Callvirt, IDisposable_Dispose); // dispose transaction (which calls changed) il.Emit(OpCodes.Ret); } } @@ -925,10 +1000,12 @@ namespace IPA.Config.Stores { // TODO: decide if i want to correct the value before or after i take the write lock var il = propSet.GetILGenerator(); + var transactionLocal = il.DeclareLocal(IDisposable_t); var GetLocal = MakeGetLocal(il); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, Impl.ImplTakeWriteMethod); // take the write lock + il.Emit(OpCodes.Call, coreChangeTransaction); // take the write lock + il.Emit(OpCodes.Stloc, transactionLocal); il.BeginExceptionBlock(); @@ -937,16 +1014,13 @@ namespace IPA.Config.Stores EmitCorrectMember(il, member, false, false, GetLocal); il.Emit(OpCodes.Call, set); - il.BeginFinallyBlock(); - - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, Impl.ImplReleaseWriteMethod); // release the write lock + il.BeginFinallyBlock(); + + il.Emit(OpCodes.Ldloc, transactionLocal); + il.Emit(OpCodes.Callvirt, IDisposable_Dispose); il.EndExceptionBlock(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, Impl.ImplInvokeChangedMethod); - il.Emit(OpCodes.Ret); } @@ -1125,6 +1199,9 @@ namespace IPA.Config.Stores il.Emit(OpCodes.Call, Type_GetTypeFromHandle); } + private static Type IDisposable_t = typeof(IDisposable); + private static MethodInfo IDisposable_Dispose = IDisposable_t.GetMethod(nameof(IDisposable.Dispose)); + private static Type Decimal_t = typeof(decimal); private static ConstructorInfo Decimal_FromFloat = Decimal_t.GetConstructor(new[] { typeof(float) }); private static ConstructorInfo Decimal_FromDouble = Decimal_t.GetConstructor(new[] { typeof(double) }); diff --git a/IPA.Loader/Loader/DisabledConfig.cs b/IPA.Loader/Loader/DisabledConfig.cs index 79013d1b..e1059c5f 100644 --- a/IPA.Loader/Loader/DisabledConfig.cs +++ b/IPA.Loader/Loader/DisabledConfig.cs @@ -23,18 +23,22 @@ namespace IPA.Loader Instance = Disabled.Generated(); } - public virtual bool Reset { get; set; } = false; + public virtual bool Reset { get; set; } = true; [NonNullable] [UseConverter(typeof(CollectionConverter>))] public virtual HashSet DisabledModIds { get; set; } = new HashSet(); protected internal virtual void Changed() { } + protected internal virtual IDisposable ChangeTransaction() => null; protected virtual void OnReload() { - if (DisabledModIds == null || Reset) - DisabledModIds = new HashSet(); + if (DisabledModIds == null || Reset) + { + DisabledModIds = new HashSet(); + Reset = false; + } } } } diff --git a/IPA.Loader/Loader/PluginManager.cs b/IPA.Loader/Loader/PluginManager.cs index 3356eba1..16c23879 100644 --- a/IPA.Loader/Loader/PluginManager.cs +++ b/IPA.Loader/Loader/PluginManager.cs @@ -98,6 +98,7 @@ namespace IPA.Loader var toDisable = transaction.ToDisable; transaction.Dispose(); + using var disabledChangeTransaction = DisabledConfig.Instance.ChangeTransaction(); { // first enable the mods that need to be void DeTree(List into, IEnumerable tree) @@ -198,7 +199,8 @@ namespace IPA.Loader result = TaskEx.WhenAll(disableStructure.Select(d => Disable(d, disabled))); } - DisabledConfig.Instance.Changed(); + //DisabledConfig.Instance.Changed(); + // changed is handled by transaction return result; } }