Browse Source

Added guarantee that enable/disable will always be from main thread

pull/46/head
Anairkoen Schno 4 years ago
parent
commit
722429c37d
6 changed files with 76 additions and 3 deletions
  1. +1
    -0
      IPA.Loader/IPA.Loader.csproj
  2. +11
    -1
      IPA.Loader/Loader/PluginManager.cs
  3. +3
    -2
      IPA.Loader/Loader/StateTransitionTransaction.cs
  4. +12
    -0
      IPA.Loader/PluginInterfaces/Attributes/LifecycleAttributes.cs
  5. +26
    -0
      IPA.Loader/Utilities/Async/Coroutines.cs
  6. +23
    -0
      IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs

+ 1
- 0
IPA.Loader/IPA.Loader.csproj View File

@ -138,6 +138,7 @@
<Compile Include="Config\ModPrefs.cs" />
<Compile Include="JsonConverters\SemverRangeConverter.cs" />
<Compile Include="JsonConverters\SemverVersionConverter.cs" />
<Compile Include="Utilities\Async\Coroutines.cs" />
<Compile Include="Utilities\Async\SingleThreadTaskScheduler.cs" />
<Compile Include="Utilities\Accessor.cs" />
<Compile Include="Utilities\Async\UnityMainThreadTaskScheduler.cs" />


+ 11
- 1
IPA.Loader/Loader/PluginManager.cs View File

@ -13,6 +13,7 @@ using Mono.Cecil;
using UnityEngine;
using Logger = IPA.Logging.Logger;
using System.Threading.Tasks;
using IPA.Utilities.Async;
#if NET4
using TaskEx = System.Threading.Tasks.Task;
using TaskEx6 = System.Threading.Tasks.Task;
@ -77,7 +78,15 @@ namespace IPA.Loader
=> new StateTransitionTransaction(AllPlugins, DisabledPlugins);
private static readonly object commitTransactionLockObject = new object();
internal static Task CommitTransaction(StateTransitionTransaction transaction)
{
if (UnityGame.OnMainThread)
return CommitTransactionInternal(transaction);
else
return UnityMainThreadTaskScheduler.Factory.StartNew(() => CommitTransactionInternal(transaction)).Unwrap();
}
private static Task CommitTransactionInternal(StateTransitionTransaction transaction)
{
lock (commitTransactionLockObject)
{
@ -181,8 +190,9 @@ namespace IPA.Loader
return TaskEx6.FromException(new CannotRuntimeDisableException(exec.Executor.Metadata));
var res = TaskEx.WhenAll(exec.Dependents.Select(d => Disable(d, alreadyDisabled)))
.ContinueWith(t => TaskEx.WhenAll(t, exec.Executor.Disable())).Unwrap();
.ContinueWith(t => TaskEx.WhenAll(t, exec.Executor.Disable()), UnityMainThreadTaskScheduler.Default).Unwrap();
// The WhenAll above allows us to wait for the executor to disable, but still propagate errors
// By scheduling on a UnityMainThreadScheduler, we ensure that Disable() is always called on the Unity main thread
alreadyDisabled.Add(exec.Executor, res);
return res;
}


+ 3
- 2
IPA.Loader/Loader/StateTransitionTransaction.cs View File

@ -209,11 +209,12 @@ namespace IPA.Loader
/// <code lang="csharp">
/// // get your transaction...
/// var complete = transaction.Commit();
/// complete.ContinueWith(t => {
/// await 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
/// });
/// </code>
/// If you are running in a coroutine, you can use <see cref="Utilities.Async.Coroutines.WaitForTask(Task)"/> instead of <see langword="await"/>.
/// </para>
/// </remarks>
/// <returns>a <see cref="Task"/> which completes whenever all disables complete</returns>


+ 12
- 0
IPA.Loader/PluginInterfaces/Attributes/LifecycleAttributes.cs View File

@ -26,6 +26,9 @@ namespace IPA
/// Typically, this will be used when the <see cref="RuntimeOptions"/> parameter of the plugins's
/// <see cref="PluginAttribute"/> is <see cref="RuntimeOptions.DynamicInit"/>.
/// </para>
/// <para>
/// The method marked by this attribute will always be called from the Unity main thread.
/// </para>
/// </remarks>
/// <seealso cref="PluginAttribute"/>
/// <seealso cref="OnStartAttribute"/>
@ -47,6 +50,9 @@ namespace IPA
/// Typically, this will be used when the <see cref="RuntimeOptions"/> parameter of the plugins's
/// <see cref="PluginAttribute"/> is <see cref="RuntimeOptions.SingleStartInit"/>.
/// </para>
/// <para>
/// The method marked by this attribute will always be called from the Unity main thread.
/// </para>
/// </remarks>
/// <seealso cref="PluginAttribute"/>
/// <seealso cref="OnEnableAttribute"/>
@ -68,6 +74,9 @@ namespace IPA
/// Typically, this will be used when the <see cref="RuntimeOptions"/> parameter of the plugins's
/// <see cref="PluginAttribute"/> is <see cref="RuntimeOptions.DynamicInit"/>.
/// </para>
/// <para>
/// The method marked by this attribute will always be called from the Unity main thread.
/// </para>
/// </remarks>
/// <seealso cref="PluginAttribute"/>
/// <seealso cref="OnExitAttribute"/>
@ -89,6 +98,9 @@ namespace IPA
/// Typically, this will be used when the <see cref="RuntimeOptions"/> parameter of the plugins's
/// <see cref="PluginAttribute"/> is <see cref="RuntimeOptions.SingleStartInit"/>.
/// </para>
/// <para>
/// The method marked by this attribute will always be called from the Unity main thread.
/// </para>
/// </remarks>
/// <seealso cref="PluginAttribute"/>
/// <seealso cref="OnDisableAttribute"/>


+ 26
- 0
IPA.Loader/Utilities/Async/Coroutines.cs View File

@ -0,0 +1,26 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IPA.Utilities.Async
{
/// <summary>
/// A class providing coroutine helpers.
/// </summary>
public static class Coroutines
{
/// <summary>
/// Stalls the coroutine until <paramref name="task"/> completes, faults, or is canceled.
/// </summary>
/// <param name="task">the <see cref="Task"/> to wait for</param>
/// <returns>a coroutine waiting for the given task</returns>
public static IEnumerator WaitForTask(Task task)
{
while (!task.IsCompleted && !task.IsCanceled && !task.IsFaulted)
yield return null;
}
}
}

+ 23
- 0
IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs View File

@ -20,6 +20,11 @@ namespace IPA.Utilities.Async
/// </summary>
/// <value>a scheduler that is managed by BSIPA</value>
public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler();
/// <summary>
/// Gets a factory for creating tasks on <see cref="Default"/>.
/// </summary>
/// <value>a factory for creating tasks on the default scheduler</value>
public static TaskFactory Factory { get; } = new TaskFactory(Default);
private readonly ConcurrentDictionary<QueueItem, Task> tasks = new ConcurrentDictionary<QueueItem, Task>();
private int queueEndPosition = 0;
@ -90,6 +95,17 @@ namespace IPA.Utilities.Async
/// <summary>
/// When used as a Unity coroutine, runs the scheduler. Otherwise, this is an invalid call.
/// </summary>
/// <remarks>
/// <para>
/// Do not ever call <see cref="UnityEngine.MonoBehaviour.StopCoroutine(IEnumerator)"/> on this
/// coroutine, nor <see cref="UnityEngine.MonoBehaviour.StopAllCoroutines"/> on the behaviour hosting
/// this coroutine. This has no way to detect this, and this object will become invalid.
/// </para>
/// <para>
/// If you need to stop this coroutine, first call <see cref="Cancel"/>, then wait for it to
/// exit on its own.
/// </para>
/// </remarks>
/// <returns>a Unity coroutine</returns>
/// <exception cref="ObjectDisposedException">if this scheduler is disposed</exception>
/// <exception cref="InvalidOperationException">if the scheduler is already running</exception>
@ -205,6 +221,10 @@ namespace IPA.Utilities.Async
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
/// <summary>
/// Disposes this object.
/// </summary>
/// <param name="disposing">whether or not to dispose managed objects</param>
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
@ -218,6 +238,9 @@ namespace IPA.Utilities.Async
}
}
/// <summary>
/// Disposes this object. This puts the object into an unusable state.
/// </summary>
// This code added to correctly implement the disposable pattern.
public void Dispose()
{


Loading…
Cancel
Save