mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-03 06:13:40 +01:00
Make ServiceScope IAsyncDisposable
ServiceScope.Dispose was not waiting for scoped services to complete disposing. This had an effect of letting a new plugin instance register a DtrBar entry before previous plugin instance's entry got unregistered. This change also cleans up unloading procedure in LocalPlugin.
This commit is contained in:
parent
fdfdee1fcb
commit
0a8f9b73fb
7 changed files with 375 additions and 184 deletions
|
|
@ -1,16 +1,18 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Serilog;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.IoC.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Container enabling the creation of scoped services.
|
||||
/// </summary>
|
||||
internal interface IServiceScope : IDisposable
|
||||
internal interface IServiceScope : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Register objects that may be injected to scoped services,
|
||||
|
|
@ -47,21 +49,57 @@ internal class ServiceScopeImpl : IServiceScope
|
|||
private readonly List<object> privateScopedObjects = [];
|
||||
private readonly ConcurrentDictionary<Type, Task<object>> scopeCreatedObjects = new();
|
||||
|
||||
private readonly ReaderWriterLockSlim disposeLock = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ServiceScopeImpl" /> class.</summary>
|
||||
/// <param name="container">The container this scope will use to create services.</param>
|
||||
public ServiceScopeImpl(ServiceContainer container) => this.container = container;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterPrivateScopes(params object[] scopes) =>
|
||||
this.privateScopedObjects.AddRange(scopes);
|
||||
public void RegisterPrivateScopes(params object[] scopes)
|
||||
{
|
||||
this.disposeLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(this.disposed, this);
|
||||
this.privateScopedObjects.AddRange(scopes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.disposeLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<object> CreateAsync(Type objectType, params object[] scopedObjects) =>
|
||||
this.container.CreateAsync(objectType, scopedObjects, this);
|
||||
public Task<object> CreateAsync(Type objectType, params object[] scopedObjects)
|
||||
{
|
||||
this.disposeLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(this.disposed, this);
|
||||
return this.container.CreateAsync(objectType, scopedObjects, this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.disposeLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) =>
|
||||
this.container.InjectProperties(instance, scopedObjects, this);
|
||||
public Task InjectPropertiesAsync(object instance, params object[] scopedObjects)
|
||||
{
|
||||
this.disposeLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(this.disposed, this);
|
||||
return this.container.InjectProperties(instance, scopedObjects, this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.disposeLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a service scoped to this scope, with private scoped objects.
|
||||
|
|
@ -69,39 +107,73 @@ internal class ServiceScopeImpl : IServiceScope
|
|||
/// <param name="objectType">The type of object to create.</param>
|
||||
/// <param name="scopedObjects">Additional scoped objects.</param>
|
||||
/// <returns>The created object, or null.</returns>
|
||||
public Task<object> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) =>
|
||||
this.scopeCreatedObjects.GetOrAdd(
|
||||
objectType,
|
||||
static (objectType, p) => p.Scope.container.CreateAsync(
|
||||
objectType,
|
||||
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()),
|
||||
(Scope: this, Objects: scopedObjects));
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
public Task<object> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects)
|
||||
{
|
||||
foreach (var objectTask in this.scopeCreatedObjects)
|
||||
this.disposeLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
objectTask.Value.ContinueWith(
|
||||
static r =>
|
||||
{
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
{
|
||||
if (r.Exception is { } e)
|
||||
Log.Error(e, "{what}: Failed to load.", nameof(ServiceScopeImpl));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (r.Result)
|
||||
{
|
||||
case IInternalDisposableService d:
|
||||
d.DisposeService();
|
||||
break;
|
||||
case IDisposable d:
|
||||
d.Dispose();
|
||||
break;
|
||||
}
|
||||
});
|
||||
ObjectDisposedException.ThrowIf(this.disposed, this);
|
||||
return this.scopeCreatedObjects.GetOrAdd(
|
||||
objectType,
|
||||
static (objectType, p) => p.Scope.container.CreateAsync(
|
||||
objectType,
|
||||
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()),
|
||||
(Scope: this, Objects: scopedObjects));
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.disposeLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
this.disposeLock.EnterWriteLock();
|
||||
this.disposed = true;
|
||||
this.disposeLock.ExitWriteLock();
|
||||
|
||||
List<Exception>? exceptions = null;
|
||||
while (!this.scopeCreatedObjects.IsEmpty)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(
|
||||
this.scopeCreatedObjects.Keys.Select(
|
||||
async type =>
|
||||
{
|
||||
if (!this.scopeCreatedObjects.Remove(type, out var serviceTask))
|
||||
return;
|
||||
|
||||
switch (await serviceTask)
|
||||
{
|
||||
case IInternalDisposableService d:
|
||||
d.DisposeService();
|
||||
break;
|
||||
case IAsyncDisposable d:
|
||||
await d.DisposeAsync();
|
||||
break;
|
||||
case IDisposable d:
|
||||
d.Dispose();
|
||||
break;
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch (AggregateException ae)
|
||||
{
|
||||
exceptions ??= [];
|
||||
exceptions.AddRange(ae.Flatten().InnerExceptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Unless Dalamud is unloading (plugin cannot be reloading at that point), ensure that there are no more
|
||||
// event callback call in progress when this function returns. Since above service dispose operations should
|
||||
// have unregistered the event listeners, on next framework tick, none can be running anymore.
|
||||
// This has an additional effect of ensuring that DtrBar entries are completely removed on return.
|
||||
// Note that this still does not handle Framework.RunOnTick with specified delays.
|
||||
await (Service<Framework>.GetNullable()?.DelayTicks(1) ?? Task.CompletedTask).SuppressException();
|
||||
|
||||
if (exceptions is not null)
|
||||
throw new AggregateException(exceptions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue