Implement DisposeAsync #8

Merged
ecenshu merged 1 commits from feature/bugfix_selflock into main 2025-12-13 01:27:07 +00:00
2 changed files with 56 additions and 32 deletions
+23 -1
View File
@@ -17,7 +17,7 @@ namespace TinfoilVibeServer.Services
/// <summary> /// <summary>
/// Reads a ROM archive (zip / 7z / rar) from a stream. /// Reads a ROM archive (zip / 7z / rar) from a stream.
/// </summary> /// </summary>
public sealed class RomArchiveReader : IDisposable public sealed class RomArchiveReader : IDisposable, IAsyncDisposable
{ {
private readonly ZipArchive? _zipArchive; private readonly ZipArchive? _zipArchive;
private readonly IArchive? _archive; private readonly IArchive? _archive;
@@ -165,9 +165,31 @@ namespace TinfoilVibeServer.Services
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
DisposeAsync().GetAwaiter().GetResult();
}
public async ValueTask DisposeAsync()
{
// Dispose ZipArchive (no async support just dispose synchronously)
if (_zipArchive is IAsyncDisposable asyncZip)
await asyncZip.DisposeAsync().ConfigureAwait(false);
else
_zipArchive?.Dispose(); _zipArchive?.Dispose();
// Dispose SharpCompress IArchive (no async support)
if (_archive is IAsyncDisposable asyncArc)
await asyncArc.DisposeAsync().ConfigureAwait(false);
else
_archive?.Dispose(); _archive?.Dispose();
// Dispose the underlying stream (may support async)
if (_archiveStream is IAsyncDisposable asyncStream)
await asyncStream.DisposeAsync().ConfigureAwait(false);
else
_archiveStream?.Dispose(); _archiveStream?.Dispose();
// Avoid doubledispose if the caller disposes again.
// (not strictly required but prevents accidental misuse)
// _zipArchive = null; // not possible fields are readonly
// _archive = null;
// _archiveStream = null;
} }
/// <summary> /// <summary>
+29 -27
View File
@@ -24,7 +24,7 @@ public interface ISnapshotService
Task RebuildSnapshotAsync(CancellationToken cancellationToken = default); Task RebuildSnapshotAsync(CancellationToken cancellationToken = default);
SnapshotService.ROMSnapshot GetSnapshot(); SnapshotService.ROMSnapshot GetSnapshot();
Task AddToSnapshotAsync(FileEntry entry); Task AddToSnapshotAsync(FileEntry entry, CancellationToken cancellationToken = default);
Task BuildSnapshotAsync(CancellationToken cancellationToken = default); Task BuildSnapshotAsync(CancellationToken cancellationToken = default);
void GetArchiveName(string titleId); void GetArchiveName(string titleId);
char GetArchivePathSeparator(); char GetArchivePathSeparator();
@@ -308,13 +308,13 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
#region Snapshot logic #region Snapshot logic
public Task AddToSnapshotAsync(FileEntry entry) public async Task AddToSnapshotAsync(FileEntry entry, CancellationToken cancellationToken = default)
{ {
// Update lookup tables // Update lookup tables
if (entry.Hash == null) if (entry.Hash == null)
{ {
_logger.LogWarning("Cannot add entry {Path} to snapshot: no hash", entry.Path); _logger.LogWarning("Cannot add entry {Path} to snapshot: no hash", entry.Path);
return Task.CompletedTask; return;
} }
var lastModified = File.GetLastWriteTimeUtc(entry.Path.Contains(ArchivePathSeparator) ? entry.Path.Split(ArchivePathSeparator)[0] : entry.Path); var lastModified = File.GetLastWriteTimeUtc(entry.Path.Contains(ArchivePathSeparator) ? entry.Path.Split(ArchivePathSeparator)[0] : entry.Path);
@@ -343,8 +343,9 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
} }
// Persist snapshot to disk // Persist snapshot to disk
PersistSnapshotAsync(); var titleIds = entry.Titles.Aggregate("", (current, titles) => $"{current} ,{titles.TitleId}");
return Task.CompletedTask; _logger.LogInformation("Added {Path} to snapshot, titleIds=[{TitleIds}]", entry.Path, titleIds);
await PersistSnapshotAsync(cancellationToken);
} }
/* ============================================================== /* ==============================================================
@@ -452,7 +453,8 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
{ {
//var titleInfo = _titleDatabaseService.GetAsync(ncaMetadataWithHash.TitleId).Result; //var titleInfo = _titleDatabaseService.GetAsync(ncaMetadataWithHash.TitleId).Result;
var fileEntryFromFileName = new FileEntry(file, fileInfo.Length, ncaMetadataWithHash.Hash, [ncaMetadataWithHash]); var fileEntryFromFileName = new FileEntry(file, fileInfo.Length, ncaMetadataWithHash.Hash, [ncaMetadataWithHash]);
AddToSnapshotAsync(fileEntryFromFileName); var addToSnapshotAsync = AddToSnapshotAsync(fileEntryFromFileName, cancellationToken);
addToSnapshotAsync.Wait(cancellationToken);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
yield return fileEntryFromFileName; yield return fileEntryFromFileName;
continue; continue;
@@ -472,7 +474,8 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
if (title != null) if (title != null)
{ {
var romEntry = new FileEntry(file, nspStreamLength, hash, [title]); var romEntry = new FileEntry(file, nspStreamLength, hash, [title]);
AddToSnapshotAsync(romEntry); var addToSnapshotAsync = AddToSnapshotAsync(romEntry, cancellationToken);
addToSnapshotAsync.Wait(cancellationToken);
titles.Add((title.TitleId, nspStreamLength, title)); titles.Add((title.TitleId, nspStreamLength, title));
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
yield return romEntry; yield return romEntry;
@@ -486,7 +489,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
if (_archiveLookup.ContainsKey(file)) continue; if (_archiveLookup.ContainsKey(file)) continue;
if (processedFiles.Contains(file)) continue; if (processedFiles.Contains(file)) continue;
_logger.LogDebug("Extracting hash for {File}", file); _logger.LogDebug("Extracting hash for {File}", file);
Stopwatch stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
hash = ComputeFirstStreamHashAsync(file, cancellationToken).Result; hash = ComputeFirstStreamHashAsync(file, cancellationToken).Result;
stopwatch.Stop(); stopwatch.Stop();
if (!string.IsNullOrEmpty(hash)) if (!string.IsNullOrEmpty(hash))
@@ -527,7 +530,8 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
foreach (var title in titles) foreach (var title in titles)
{ {
var archiveEntry = new FileEntry(file + ArchivePathSeparator + title.Item1, title.Item2, title.Item3.Hash, [title.Item3]); var archiveEntry = new FileEntry(file + ArchivePathSeparator + title.Item1, title.Item2, title.Item3.Hash, [title.Item3]);
AddToSnapshotAsync(archiveEntry); var addToSnapshotAsync = AddToSnapshotAsync(archiveEntry, cancellationToken);
addToSnapshotAsync.Wait(cancellationToken);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
yield return archiveEntry; yield return archiveEntry;
} }
@@ -564,11 +568,15 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
} }
catch (IOException ex) when (attempt < _options.MaxRetryCount - 1) catch (IOException ex) when (attempt < _options.MaxRetryCount - 1)
{ {
var delay = (int)((attempt+1) * _options.DebounceTimeoutMs * _options.RetryMultiplier); var delay = (int)((attempt + 1) * _options.DebounceTimeoutMs * _options.RetryMultiplier);
_logger.LogWarning(ex, "Attempt {Attempt} failed for {Path}. Retrying after {Delay}.", _logger.LogWarning(ex, "Failed to load {Path}. Attempt {Attempt}, Retrying after {Delay}.",
attempt + 1, file, delay); file, attempt + 1, delay);
await Task.Delay(delay, cancellationToken); await Task.Delay(delay, cancellationToken);
} }
catch (IOException) when (attempt >= _options.MaxRetryCount - 1)
{
_logger.LogWarning("Load {Path} failed after {retries} attempts", file, attempt + 1);
}
} }
return null; return null;
} }
@@ -859,7 +867,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
// The first stream is the first entry returned by GetContentInfos(). // The first stream is the first entry returned by GetContentInfos().
try try
{ {
using var reader = new RomArchiveReader(filePath); await using var reader = new RomArchiveReader(filePath);
var first = reader.GetEntries().FirstOrDefault(); var first = reader.GetEntries().FirstOrDefault();
if (first == null) return ComputeFullHash(filePath); if (first == null) return ComputeFullHash(filePath);
@@ -871,35 +879,29 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
} }
catch catch
{ {
// On error, fall back to the full file hash // ignored
}
}
await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var ncaMetadataWithHash = _nspExtractor.ExtractFromStream(fs); var ncaMetadataWithHash = _nspExtractor.ExtractFromStream(fs);
return ncaMetadataWithHash?.Hash ?? string.Empty; return ncaMetadataWithHash?.Hash ?? string.Empty;
} }
}
else
{
await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var ncaMetadataWithHash = _nspExtractor.ExtractFromStream(fs);
return ncaMetadataWithHash?.Hash ?? string.Empty;
}
}
catch (IOException ex) when (attempt < _options.MaxRetryCount - 1) catch (IOException ex) when (attempt < _options.MaxRetryCount - 1)
{ {
var delay = (int)((attempt + 1) * _options.DebounceTimeoutMs * _options.RetryMultiplier); var delay = (int)((attempt + 1) * _options.DebounceTimeoutMs * _options.RetryMultiplier);
_logger.LogWarning(ex, "Attempt {Attempt} failed for {Path}. Retrying after {Delay}.", _logger.LogWarning(ex, "Failed to load {Path}. Attempt {Attempt}, Retrying after {Delay}.",
attempt + 1, filePath, delay); filePath, attempt + 1, delay);
await Task.Delay(delay, cancellationToken); await Task.Delay(delay, cancellationToken);
} }
catch (IOException) catch (IOException) when (attempt >= _options.MaxRetryCount - 1)
{ {
_logger.LogWarning("Attempt to load {Path} failed after {retries}", filePath, attempt + 1); _logger.LogWarning("Load {Path} failed after {retries} attempts", filePath, attempt + 1);
return null; return null;
} }
} }
return string.Empty; return string.Empty;
throw new IOException($"Failed to compute hash for {filePath} after {_options.MaxRetryCount} attempts");
} }
private static string ComputeFullHash(string filePath) private static string ComputeFullHash(string filePath)