diff --git a/src/Files.App.Storage/Legacy/RecycleBinWatcher.cs b/src/Files.App.Storage/Legacy/RecycleBinWatcher.cs index c312fe8e5570..853c7b40e5dd 100644 --- a/src/Files.App.Storage/Legacy/RecycleBinWatcher.cs +++ b/src/Files.App.Storage/Legacy/RecycleBinWatcher.cs @@ -39,36 +39,46 @@ public void StartWatcher() { // NOTE: SHChangeNotifyRegister only works if recycle bin is open in File Explorer. + foreach (var drive in SystemIO.DriveInfo.GetDrives()) + StartWatcher(drive.Name); + } + + /// + /// Starts watching the Recycle Bin on the given drive if it isn't watched already. + /// Re-arms monitoring after a failed eject and when a drive appears after startup. + /// + public void StartWatcher(string driveRoot) + { // Listen changes only on the Recycle Bin that the current logon user has var sid = WindowsIdentity.GetCurrent().User?.ToString() ?? string.Empty; if (string.IsNullOrEmpty(sid)) return; - foreach (var drive in SystemIO.DriveInfo.GetDrives()) + // NOTE: Suppressed NullReferenceException caused by EnableRaisingEvents in #15808 + SafetyExtensions.IgnoreExceptions(() => { - var recyclePath = SystemIO.Path.Combine(drive.Name, "$RECYCLE.BIN", sid); + var recyclePath = SystemIO.Path.Combine(driveRoot, "$RECYCLE.BIN", sid); - if (drive.DriveType is SystemIO.DriveType.Network || + if (new SystemIO.DriveInfo(driveRoot).DriveType is SystemIO.DriveType.Network || !SystemIO.Directory.Exists(recyclePath)) - continue; + return; + + if (_watchers.Any(x => string.Equals(x.Path, recyclePath, StringComparison.OrdinalIgnoreCase))) + return; - // NOTE: Suppressed NullReferenceException caused by EnableRaisingEvents in #15808 - SafetyExtensions.IgnoreExceptions(() => + SystemIO.FileSystemWatcher watcher = new() { - SystemIO.FileSystemWatcher watcher = new() - { - Path = recyclePath, - Filter = "*.*", - NotifyFilter = SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName | SystemIO.NotifyFilters.DirectoryName - }; - - watcher.Created += Watcher_Changed; - watcher.Deleted += Watcher_Changed; - watcher.EnableRaisingEvents = true; - - _watchers.Add(watcher); - }); - } + Path = recyclePath, + Filter = "*.*", + NotifyFilter = SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName | SystemIO.NotifyFilters.DirectoryName + }; + + watcher.Created += Watcher_Changed; + watcher.Deleted += Watcher_Changed; + watcher.EnableRaisingEvents = true; + + _watchers.Add(watcher); + }); } /// @@ -78,6 +88,21 @@ public void StopWatcher() watcher.Dispose(); } + /// + /// Stops watching the Recycle Bin on the given drive so the watcher's handle can't block ejection. + /// + public void StopWatcher(string driveRoot) + { + for (int i = _watchers.Count - 1; i >= 0; i--) + { + if (_watchers[i].Path.StartsWith(driveRoot, StringComparison.OrdinalIgnoreCase)) + { + _watchers[i].Dispose(); + _watchers.RemoveAt(i); + } + } + } + private void Watcher_Changed(object sender, SystemIO.FileSystemEventArgs e) { // Don't listen changes on files starting with '$I' diff --git a/src/Files.App/Data/Models/DrivesViewModel.cs b/src/Files.App/Data/Models/DrivesViewModel.cs index 9cf463c636d7..d84dfdad9030 100644 --- a/src/Files.App/Data/Models/DrivesViewModel.cs +++ b/src/Files.App/Data/Models/DrivesViewModel.cs @@ -93,6 +93,10 @@ private void Watcher_DeviceAdded(object? sender, IFolder e) Drives.Add(e); } + // Watch the Recycle Bin on drives that appear after startup (including reinserted ones) + if (!string.IsNullOrEmpty(e.Id)) + Ioc.Default.GetRequiredService().Watcher.StartWatcher(e.Id.EndsWith('\\') ? e.Id : e.Id + '\\'); + Watcher_EnumerationCompleted(null, EventArgs.Empty); } diff --git a/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs b/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs index e1d4c36e040f..b296644851b6 100644 --- a/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs @@ -15,7 +15,52 @@ public static class DriveHelpers { public static async void EjectDeviceAsync(string path) { + var driveRoot = path.EndsWith('\\') ? path : path + '\\'; + + ReleaseDriveHandles(driveRoot); + + // Give the released handles a moment to close before the shell issues the removal query + await Task.Delay(300); + await ContextMenu.InvokeVerb("eject", path); + + // If the volume is still mounted the eject was vetoed; re-arm the Recycle Bin watcher + await Task.Delay(2000); + if (SystemIO.Directory.Exists(driveRoot)) + Ioc.Default.GetRequiredService().Watcher.StartWatcher(driveRoot); + } + + /// + /// Releases the handles Files itself holds on the drive (directory change watchers, sidebar + /// subtree watchers, the Recycle Bin watcher) so they can't veto the device removal. + /// + private static void ReleaseDriveHandles(string driveRoot) + { + // Navigate every pane showing the drive to Home and close its directory watcher + var multitaskingContext = Ioc.Default.GetRequiredService(); + foreach (var tab in multitaskingContext.Control?.GetAllTabInstances() ?? []) + { + if (tab is not ShellPanesPage panesPage) + continue; + + foreach (var pane in panesPage.GetPanes()) + { + var panePath = pane.ShellViewModel?.CurrentFolder?.ItemPath; + if (panePath is not null && (panePath + '\\').StartsWith(driveRoot, StringComparison.OrdinalIgnoreCase)) + { + pane.ShellViewModel?.CloseWatcher(); + pane.NavigateHome(); + } + } + } + + // Stop sidebar subtree watchers rooted on the drive + var drivesViewModel = Ioc.Default.GetRequiredService(); + if (drivesViewModel.Drives.Cast().FirstOrDefault(x => string.Equals(x.Path?.TrimEnd('\\'), driveRoot.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase)) is { } driveItem) + driveItem.StopWatchingSubfoldersAndDescendants(); + + // Drop the Recycle Bin watcher for this drive + Ioc.Default.GetRequiredService().Watcher.StopWatcher(driveRoot); } public static async Task CheckEmptyDrive(string? drivePath)