mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
* Add IInternal/PublicDisposableService Plugins are exposed interfaces that are not inherited from `IDisposable`, but services implementing plugin interfaces often implement `IDisposable`. Some plugins may try to call `IDisposable.Dispose` on everything provided, and it also is possible to use `using` clause too eagerly while working on Dalamud itself, such as writing `using var smth = await Service<SomeService>.GetAsync();`. Such behaviors often lead to a difficult-to-debug errors, and making those services either not an `IDisposable` or making `IDisposable.Dispose` do nothing if the object has been loaded would prevent such errors. As `ServiceManager` must be the only class dealing with construction and disposal of services, `IInternalDisposableService` has been added to limit who can dispose the object. `IPublicDisposableService` also has been added to classes that can be constructed and accessed directly by plugins; for those, `Dispose` will be ignored if the instance is a service instance, and only `DisposeService` will respond. In addition, `DalamudPluginInterface` and `UiBuilder` also have been changed so that their `IDisposable.Dispose` no longer respond, and instead, internal functions have been added to only allow disposal from Dalamud. * Cleanup * Postmerge fixes * More explanation on RunOnFrameworkThread(ClearHooks) * Mark ReliableFileStorage public ctor obsolete --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com>
374 lines
14 KiB
C#
374 lines
14 KiB
C#
using System.Buffers;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using Dalamud.Interface.Internal;
|
|
using Dalamud.IoC;
|
|
using Dalamud.IoC.Internal;
|
|
using Dalamud.Networking.Http;
|
|
using Dalamud.Utility;
|
|
using Dalamud.Utility.Timing;
|
|
|
|
using JetBrains.Annotations;
|
|
|
|
using Serilog;
|
|
|
|
namespace Dalamud.Storage.Assets;
|
|
|
|
/// <summary>
|
|
/// A concrete class for <see cref="IDalamudAssetManager"/>.
|
|
/// </summary>
|
|
[PluginInterface]
|
|
[ServiceManager.BlockingEarlyLoadedService]
|
|
#pragma warning disable SA1015
|
|
[ResolveVia<IDalamudAssetManager>]
|
|
#pragma warning restore SA1015
|
|
internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamudAssetManager
|
|
{
|
|
private const int DownloadAttemptCount = 10;
|
|
private const int RenameAttemptCount = 10;
|
|
|
|
private readonly object syncRoot = new();
|
|
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
|
|
private readonly Dictionary<DalamudAsset, Task<FileStream>?> fileStreams;
|
|
private readonly Dictionary<DalamudAsset, Task<IDalamudTextureWrap>?> textureWraps;
|
|
private readonly Dalamud dalamud;
|
|
private readonly HappyHttpClient httpClient;
|
|
private readonly string localSourceDirectory;
|
|
private readonly CancellationTokenSource cancellationTokenSource;
|
|
|
|
private bool isDisposed;
|
|
|
|
[ServiceManager.ServiceConstructor]
|
|
private DalamudAssetManager(
|
|
Dalamud dalamud,
|
|
HappyHttpClient httpClient,
|
|
ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker)
|
|
{
|
|
this.dalamud = dalamud;
|
|
this.httpClient = httpClient;
|
|
this.localSourceDirectory = Path.Combine(this.dalamud.AssetDirectory.FullName, "..", "local");
|
|
Directory.CreateDirectory(this.localSourceDirectory);
|
|
this.scopedFinalizer.Add(this.cancellationTokenSource = new());
|
|
|
|
this.fileStreams = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<FileStream>?)null);
|
|
this.textureWraps = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<IDalamudTextureWrap>?)null);
|
|
|
|
// Block until all the required assets to be ready.
|
|
var loadTimings = Timings.Start("DAM LoadAll");
|
|
registerStartupBlocker(
|
|
Task.WhenAll(
|
|
Enum.GetValues<DalamudAsset>()
|
|
.Where(x => x is not DalamudAsset.Empty4X4)
|
|
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is true)
|
|
.Select(this.CreateStreamAsync)
|
|
.Select(x => x.ToContentDisposedTask()))
|
|
.ContinueWith(
|
|
r =>
|
|
{
|
|
loadTimings.Dispose();
|
|
return r;
|
|
})
|
|
.Unwrap(),
|
|
"Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available.");
|
|
|
|
Task.WhenAll(
|
|
Enum.GetValues<DalamudAsset>()
|
|
.Where(x => x is not DalamudAsset.Empty4X4)
|
|
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is false)
|
|
.Select(this.CreateStreamAsync)
|
|
.Select(x => x.ToContentDisposedTask(true)))
|
|
.ContinueWith(r => Log.Verbose($"Optional assets load state: {r}"));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4);
|
|
|
|
/// <inheritdoc/>
|
|
void IInternalDisposableService.DisposeService()
|
|
{
|
|
lock (this.syncRoot)
|
|
{
|
|
if (this.isDisposed)
|
|
return;
|
|
|
|
this.isDisposed = true;
|
|
}
|
|
|
|
this.cancellationTokenSource.Cancel();
|
|
Task.WaitAll(
|
|
Array.Empty<Task>()
|
|
.Concat(this.fileStreams.Values)
|
|
.Concat(this.textureWraps.Values)
|
|
.Where(x => x is not null)
|
|
.Select(x => x.ContinueWith(r => { _ = r.Exception; }))
|
|
.ToArray());
|
|
this.scopedFinalizer.Dispose();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
[Pure]
|
|
public bool IsStreamImmediatelyAvailable(DalamudAsset asset) =>
|
|
asset.GetAttribute<DalamudAssetAttribute>()?.Data is not null
|
|
|| this.fileStreams[asset]?.IsCompletedSuccessfully is true;
|
|
|
|
/// <inheritdoc/>
|
|
[Pure]
|
|
public Stream CreateStream(DalamudAsset asset)
|
|
{
|
|
var s = this.CreateStreamAsync(asset);
|
|
s.Wait();
|
|
if (s.IsCompletedSuccessfully)
|
|
return s.Result;
|
|
if (s.Exception is not null)
|
|
throw new AggregateException(s.Exception.InnerExceptions);
|
|
throw new OperationCanceledException();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
[Pure]
|
|
public Task<Stream> CreateStreamAsync(DalamudAsset asset)
|
|
{
|
|
if (asset.GetAttribute<DalamudAssetAttribute>() is { Data: { } rawData })
|
|
return Task.FromResult<Stream>(new MemoryStream(rawData, false));
|
|
|
|
Task<FileStream> task;
|
|
lock (this.syncRoot)
|
|
{
|
|
if (this.isDisposed)
|
|
throw new ObjectDisposedException(nameof(DalamudAssetManager));
|
|
|
|
task = this.fileStreams[asset] ??= CreateInnerAsync();
|
|
}
|
|
|
|
return this.TransformImmediate(
|
|
task,
|
|
x => (Stream)new FileStream(
|
|
x.Name,
|
|
FileMode.Open,
|
|
FileAccess.Read,
|
|
FileShare.Read,
|
|
4096,
|
|
FileOptions.Asynchronous | FileOptions.SequentialScan));
|
|
|
|
async Task<FileStream> CreateInnerAsync()
|
|
{
|
|
string path;
|
|
List<Exception?> exceptions = null;
|
|
foreach (var name in asset.GetAttributes<DalamudAssetPathAttribute>().Select(x => x.FileName))
|
|
{
|
|
if (!File.Exists(path = Path.Combine(this.dalamud.AssetDirectory.FullName, name)))
|
|
continue;
|
|
|
|
try
|
|
{
|
|
return File.OpenRead(path);
|
|
}
|
|
catch (Exception e) when (e is not OperationCanceledException)
|
|
{
|
|
exceptions ??= new();
|
|
exceptions.Add(e);
|
|
}
|
|
}
|
|
|
|
if (File.Exists(path = Path.Combine(this.localSourceDirectory, asset.ToString())))
|
|
{
|
|
try
|
|
{
|
|
return File.OpenRead(path);
|
|
}
|
|
catch (Exception e) when (e is not OperationCanceledException)
|
|
{
|
|
exceptions ??= new();
|
|
exceptions.Add(e);
|
|
}
|
|
}
|
|
|
|
var tempPath = $"{path}.{Environment.ProcessId:x}.{Environment.CurrentManagedThreadId:x}";
|
|
try
|
|
{
|
|
for (var i = 0; i < DownloadAttemptCount; i++)
|
|
{
|
|
var attemptedAny = false;
|
|
foreach (var url in asset.GetAttributes<DalamudAssetOnlineSourceAttribute>())
|
|
{
|
|
Log.Information("[{who}] {asset}: Trying {url}", nameof(DalamudAssetManager), asset, url);
|
|
attemptedAny = true;
|
|
|
|
try
|
|
{
|
|
await using (var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write))
|
|
{
|
|
await url.DownloadAsync(
|
|
this.httpClient.SharedHttpClient,
|
|
tempPathStream,
|
|
this.cancellationTokenSource.Token);
|
|
}
|
|
|
|
for (var j = RenameAttemptCount; ; j--)
|
|
{
|
|
try
|
|
{
|
|
File.Move(tempPath, path);
|
|
}
|
|
catch (IOException ioe)
|
|
{
|
|
if (j == 0)
|
|
throw;
|
|
Log.Warning(
|
|
ioe,
|
|
"[{who}] {asset}: Renaming failed; trying again {n} more times",
|
|
nameof(DalamudAssetManager),
|
|
asset,
|
|
j);
|
|
await Task.Delay(1000, this.cancellationTokenSource.Token);
|
|
continue;
|
|
}
|
|
|
|
return File.OpenRead(path);
|
|
}
|
|
}
|
|
catch (Exception e) when (e is not OperationCanceledException)
|
|
{
|
|
Log.Error(e, "[{who}] {asset}: Failed {url}", nameof(DalamudAssetManager), asset, url);
|
|
}
|
|
}
|
|
|
|
if (!attemptedAny)
|
|
throw new FileNotFoundException($"Failed to find the asset {asset}.", asset.ToString());
|
|
|
|
// Wait up to 5 minutes
|
|
var delay = Math.Min(300, (1 << i) * 1000);
|
|
Log.Error(
|
|
"[{who}] {asset}: Failed to download. Trying again in {sec} seconds...",
|
|
nameof(DalamudAssetManager),
|
|
asset,
|
|
delay);
|
|
await Task.Delay(delay * 1000, this.cancellationTokenSource.Token);
|
|
}
|
|
|
|
throw new FileNotFoundException($"Failed to load the asset {asset}.", asset.ToString());
|
|
}
|
|
catch (Exception e) when (e is not OperationCanceledException)
|
|
{
|
|
exceptions ??= new();
|
|
exceptions.Add(e);
|
|
try
|
|
{
|
|
File.Delete(tempPath);
|
|
}
|
|
catch
|
|
{
|
|
// don't care
|
|
}
|
|
}
|
|
|
|
throw new AggregateException(exceptions);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
[Pure]
|
|
public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) =>
|
|
this.GetDalamudTextureWrapAsync(asset).Result;
|
|
|
|
/// <inheritdoc/>
|
|
[Pure]
|
|
[return: NotNullIfNotNull(nameof(defaultWrap))]
|
|
public IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap)
|
|
{
|
|
var task = this.GetDalamudTextureWrapAsync(asset);
|
|
return task.IsCompletedSuccessfully ? task.Result : defaultWrap;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
[Pure]
|
|
public Task<IDalamudTextureWrap> GetDalamudTextureWrapAsync(DalamudAsset asset)
|
|
{
|
|
var purpose = asset.GetPurpose();
|
|
if (purpose is not DalamudAssetPurpose.TextureFromPng and not DalamudAssetPurpose.TextureFromRaw)
|
|
throw new ArgumentOutOfRangeException(nameof(asset), asset, "The asset cannot be taken as a Texture2D.");
|
|
|
|
Task<IDalamudTextureWrap> task;
|
|
lock (this.syncRoot)
|
|
{
|
|
if (this.isDisposed)
|
|
throw new ObjectDisposedException(nameof(DalamudAssetManager));
|
|
|
|
task = this.textureWraps[asset] ??= CreateInnerAsync();
|
|
}
|
|
|
|
return task;
|
|
|
|
async Task<IDalamudTextureWrap> CreateInnerAsync()
|
|
{
|
|
var buf = Array.Empty<byte>();
|
|
try
|
|
{
|
|
var im = (await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync()).Manager;
|
|
await using var stream = await this.CreateStreamAsync(asset);
|
|
var length = checked((int)stream.Length);
|
|
buf = ArrayPool<byte>.Shared.Rent(length);
|
|
stream.ReadExactly(buf, 0, length);
|
|
var image = purpose switch
|
|
{
|
|
DalamudAssetPurpose.TextureFromPng => im.LoadImage(buf),
|
|
DalamudAssetPurpose.TextureFromRaw =>
|
|
asset.GetAttribute<DalamudAssetRawTextureAttribute>() is { } raw
|
|
? im.LoadImageFromDxgiFormat(buf, raw.Pitch, raw.Width, raw.Height, raw.Format)
|
|
: throw new InvalidOperationException(
|
|
"TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."),
|
|
_ => null,
|
|
};
|
|
var disposeDeferred =
|
|
this.scopedFinalizer.Add(image)
|
|
?? throw new InvalidOperationException("Something went wrong very badly");
|
|
return new DisposeSuppressingDalamudTextureWrap(disposeDeferred);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e, "[{name}] Failed to load {asset}.", nameof(DalamudAssetManager), asset);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
private Task<TOut> TransformImmediate<TIn, TOut>(Task<TIn> task, Func<TIn, TOut> transformer)
|
|
{
|
|
if (task.IsCompletedSuccessfully)
|
|
return Task.FromResult(transformer(task.Result));
|
|
if (task.Exception is { } exc)
|
|
return Task.FromException<TOut>(exc);
|
|
return task.ContinueWith(_ => this.TransformImmediate(task, transformer)).Unwrap();
|
|
}
|
|
|
|
private class DisposeSuppressingDalamudTextureWrap : IDalamudTextureWrap
|
|
{
|
|
private readonly IDalamudTextureWrap innerWrap;
|
|
|
|
public DisposeSuppressingDalamudTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap;
|
|
|
|
/// <inheritdoc/>
|
|
public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle;
|
|
|
|
/// <inheritdoc/>
|
|
public int Width => this.innerWrap.Width;
|
|
|
|
/// <inheritdoc/>
|
|
public int Height => this.innerWrap.Height;
|
|
|
|
/// <inheritdoc/>
|
|
public void Dispose()
|
|
{
|
|
// suppressed
|
|
}
|
|
}
|
|
}
|