diff --git a/src/ManagedDrive.App/App.xaml.cs b/src/ManagedDrive.App/App.xaml.cs
index c7354ab..ff34ee2 100644
--- a/src/ManagedDrive.App/App.xaml.cs
+++ b/src/ManagedDrive.App/App.xaml.cs
@@ -11,6 +11,11 @@ public partial class App
{
private const string SingleInstanceMutexName = "Global\\ManagedDrive-4A7C2E1B-9F3D-4B8A-A1C5-3E6D2F0B8C9A";
+ ///
+ /// Upper bound on how long waits for all disk saves to finish.
+ ///
+ private static readonly TimeSpan SessionEndingSaveTimeout = TimeSpan.FromSeconds(10);
+
private readonly DispatcherTimer _timerPollCursor = new();
private readonly DispatcherTimer _timerShowTrayInfoPopup = new();
private readonly DispatcherTimer _timerTooltipCooldown = new();
@@ -406,9 +411,14 @@ private void OnDiskSaveFailed(object? sender, Exception ex)
///
/// Fires when Windows is logging off, shutting down, or restarting. WPF's own
/// Exit 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 RamDisk 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
+ /// 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.
///
private void OnSessionEnding(object sender, SessionEndingEventArgs e)
{
@@ -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()
diff --git a/src/ManagedDrive.Core/RamDisk.cs b/src/ManagedDrive.Core/RamDisk.cs
index e440bbb..1f551a6 100644
--- a/src/ManagedDrive.Core/RamDisk.cs
+++ b/src/ManagedDrive.Core/RamDisk.cs
@@ -275,7 +275,7 @@ public void Dispose()
{
try
{
- if (_fs.IsDirty || Options.PersistImagePath != _lastSavedImagePath)
+ if (NeedsSave())
{
SaveToImage();
}
@@ -370,13 +370,19 @@ public void SaveToImage()
/// Saves the disk image while holding , 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
- /// is null.
+ /// is null, 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 ) — this keeps unmodified disks
+ /// out of the OS shutdown time budget.
///
public void SaveToImageSafe()
{
lock (_autoSaveLock)
{
- SaveToImage();
+ if (NeedsSave())
+ {
+ SaveToImage();
+ }
}
}
@@ -599,6 +605,14 @@ private void ConfigureAutoSaveTimer()
}
}
+ ///
+ /// 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
+ /// and so both skip saving under the
+ /// same condition.
+ ///
+ private bool NeedsSave() => _fs.IsDirty || Options.PersistImagePath != _lastSavedImagePath;
+
///
/// 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
@@ -615,7 +629,7 @@ private void TryAutoSave()
try
{
- if (_fs.IsDirty || Options.PersistImagePath != _lastSavedImagePath)
+ if (NeedsSave())
{
SaveToImage();
TryWriteSnapshot();