Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions src/ManagedDrive.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ public partial class App
{
private const string SingleInstanceMutexName = "Global\\ManagedDrive-4A7C2E1B-9F3D-4B8A-A1C5-3E6D2F0B8C9A";

/// <summary>
/// Upper bound on how long <see cref="OnSessionEnding"/> waits for all disk saves to finish.
/// </summary>
private static readonly TimeSpan SessionEndingSaveTimeout = TimeSpan.FromSeconds(10);

private readonly DispatcherTimer _timerPollCursor = new();
private readonly DispatcherTimer _timerShowTrayInfoPopup = new();
private readonly DispatcherTimer _timerTooltipCooldown = new();
Expand Down Expand Up @@ -406,9 +411,14 @@ private void OnDiskSaveFailed(object? sender, Exception ex)
/// <summary>
/// Fires when Windows is logging off, shutting down, or restarting. WPF's own
/// <c>Exit</c> event does not fire in this case, and the OS may kill the process shortly
/// after this callback returns, so save every mounted disk's image synchronously and
/// without unmounting (unmounting is unnecessary here and would risk exceeding the
/// shutdown time budget).
/// after this callback returns, so save every mounted disk's image and without unmounting
/// (unmounting is unnecessary here and would risk exceeding the shutdown time budget).
/// Disks are saved in parallel rather than sequentially — each disk's save is independent
/// file I/O guarded by its own <c>RamDisk</c> lock, so running them concurrently lets the
/// total time fit the shutdown budget even with several large/compressed/encrypted disks.
/// This method blocks synchronously until all saves finish or <see cref="SessionEndingSaveTimeout"/>
/// elapses, whichever comes first — a bound is needed so a single stuck save (e.g. a backing
/// path that has become unreachable) can't hang this callback indefinitely.
/// </summary>
private void OnSessionEnding(object sender, SessionEndingEventArgs e)
{
Expand All @@ -417,17 +427,21 @@ private void OnSessionEnding(object sender, SessionEndingEventArgs e)
return;
}

foreach (var disk in _mountManager.GetAll())
{
try
var saveTasks = _mountManager.GetAll()
.Select(disk => Task.Run(() =>
{
disk.SaveToImageSafe();
}
catch
{
// Best-effort, matches RamDisk.Dispose()/TryAutoSave() swallow pattern.
}
}
try
{
disk.SaveToImageSafe();
}
catch
{
// Best-effort, matches RamDisk.Dispose()/TryAutoSave() swallow pattern.
}
}))
.ToArray();

Task.WaitAll(saveTasks, SessionEndingSaveTimeout);
}

private void PositionTrayPopup()
Expand Down
22 changes: 18 additions & 4 deletions src/ManagedDrive.Core/RamDisk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ public void Dispose()
{
try
{
if (_fs.IsDirty || Options.PersistImagePath != _lastSavedImagePath)
if (NeedsSave())
{
SaveToImage();
}
Expand Down Expand Up @@ -370,13 +370,19 @@ public void SaveToImage()
/// Saves the disk image while holding <see cref="_autoSaveLock"/>, without unmounting.
/// Intended for external shutdown-notification callers (e.g. Windows session-ending) that
/// need a quick, safe save that can't race the periodic auto-save tick. Does nothing if
/// <see cref="DiskOptions.PersistImagePath"/> is <c>null</c>.
/// <see cref="DiskOptions.PersistImagePath"/> is <c>null</c>, or if the disk's content hasn't
/// changed since the last successful save and the configured image path hasn't changed
/// either (same skip condition as <see cref="TryAutoSave"/>) — this keeps unmodified disks
/// out of the OS shutdown time budget.
/// </summary>
public void SaveToImageSafe()
{
lock (_autoSaveLock)
{
SaveToImage();
if (NeedsSave())
{
SaveToImage();
}
}
}

Expand Down Expand Up @@ -599,6 +605,14 @@ private void ConfigureAutoSaveTimer()
}
}

/// <summary>
/// Whether a save would actually write anything: the disk has unsaved changes, or the
/// configured persist path has changed since the last successful save. Shared by
/// <see cref="SaveToImageSafe"/> and <see cref="TryAutoSave"/> so both skip saving under the
/// same condition.
/// </summary>
private bool NeedsSave() => _fs.IsDirty || Options.PersistImagePath != _lastSavedImagePath;

/// <summary>
/// Saves the disk image on the periodic timer tick, swallowing any exception so a failed
/// save does not affect the mounted disk or crash the timer thread. If the previous tick's
Expand All @@ -615,7 +629,7 @@ private void TryAutoSave()

try
{
if (_fs.IsDirty || Options.PersistImagePath != _lastSavedImagePath)
if (NeedsSave())
{
SaveToImage();
TryWriteSnapshot();
Expand Down
Loading