Implement DisposeAsync
Log when snapshot is added
This commit is contained in:
@@ -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()
|
||||||
{
|
{
|
||||||
_zipArchive?.Dispose();
|
DisposeAsync().GetAwaiter().GetResult();
|
||||||
_archive?.Dispose();
|
}
|
||||||
_archiveStream?.Dispose();
|
|
||||||
|
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();
|
||||||
|
// Dispose SharpCompress IArchive (no async support)
|
||||||
|
if (_archive is IAsyncDisposable asyncArc)
|
||||||
|
await asyncArc.DisposeAsync().ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
_archive?.Dispose();
|
||||||
|
// Dispose the underlying stream (may support async)
|
||||||
|
if (_archiveStream is IAsyncDisposable asyncStream)
|
||||||
|
await asyncStream.DisposeAsync().ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
_archiveStream?.Dispose();
|
||||||
|
// Avoid double‑dispose 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>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
var ncaMetadataWithHash = _nspExtractor.ExtractFromStream(fs);
|
|
||||||
return ncaMetadataWithHash?.Hash ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user