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();