diff --git a/PKHeX.Core/Saves/Util/SaveFinder.cs b/PKHeX.Core/Saves/Util/SaveFinder.cs index 8d8ece247..7d43bac36 100644 --- a/PKHeX.Core/Saves/Util/SaveFinder.cs +++ b/PKHeX.Core/Saves/Util/SaveFinder.cs @@ -18,27 +18,70 @@ public static class SaveFinder /// List of drives on the host machine. /// Optional parameter to skip the first drive. /// The first drive is usually the system hard drive, or can be a floppy disk drive (slower to check, never has expected data). + /// Cancellation token to cancel the operation. /// Folder path pointing to the Nintendo 3DS folder. - public static string? Get3DSLocation(IEnumerable drives, bool skipFirstDrive = true) => - FindConsoleRootFolder(drives, "Nintendo 3DS", skipFirstDrive); + public static string? Get3DSLocation(IEnumerable drives, bool skipFirstDrive, CancellationToken token) => + FindConsoleRootFolder(drives, "Nintendo 3DS", skipFirstDrive, token); /// /// Searches the provided to find a valid Switch drive, usually from an inserted SD card. /// /// List of drives on the host machine. - /// Optional parameter to skip the first drive. - /// The first drive is usually the system hard drive, or can be a floppy disk drive (slower to check, never has expected data). + /// Optional parameter to skip the first drive. + /// Cancellation token to cancel the operation. /// Folder path pointing to the Nintendo folder. - public static string? GetSwitchLocation(IEnumerable drives, bool skipFirstDrive = true) => - FindConsoleRootFolder(drives, "Nintendo", skipFirstDrive); + public static string? GetSwitchLocation(IEnumerable drives, bool skipFirstDrive, CancellationToken token) => + FindConsoleRootFolder(drives, "Nintendo", skipFirstDrive, token); - private static string? FindConsoleRootFolder(IEnumerable drives, [ConstantExpected] string path, bool skipFirstDrive) + private static string? FindConsoleRootFolder(IEnumerable drives, [ConstantExpected] string path, bool skipFirstDrive, CancellationToken token) { - if (skipFirstDrive) - drives = drives.Skip(1); + foreach (var drive in GetUsableDrives(drives, skipFirstDrive, token)) + { + if (token.IsCancellationRequested) + break; - var paths = drives.Select(drive => Path.Combine(drive, path)); - return paths.FirstOrDefault(Directory.Exists); + var candidate = Path.Combine(drive, path); + // Directory.Exists is synchronous and cannot itself be cancelled. + // Slow/unresponsive external drives can really drag execution. + if (Directory.Exists(candidate)) + return candidate; + } + + return null; + } + + private static IEnumerable GetUsableDrives(IEnumerable drives, bool skipFirstDrive, CancellationToken token) + { + var first = true; + foreach (var drive in drives) + { + if (token.IsCancellationRequested) + yield break; + + if (skipFirstDrive && first) + { + first = false; + continue; + } + + first = false; + if (IsUsableDrive(drive)) + yield return drive; + } + } + + private static bool IsUsableDrive(string drive) + { + try + { + var type = new DriveInfo(drive).DriveType; + return type is DriveType.Fixed or DriveType.Removable or DriveType.Ram; + // ignore Network, CDRom, NoRootDirectory, and Unknown + } + catch + { + return false; + } } /// @@ -92,15 +135,28 @@ public static IEnumerable GetSwitchBackupPaths(string root) /// Reference to a valid save file, if any. public static SaveFile? FindMostRecentSaveFile(IReadOnlyList drives, IEnumerable extra, CancellationToken token) { + if (token.IsCancellationRequested) + return null; + var foldersToCheck = GetFoldersToCheck(drives, extra, token); var result = GetSaveFilePathsFromFolders(foldersToCheck, true, out var possiblePaths, token); if (!result) throw new FileNotFoundException(string.Join(Environment.NewLine, possiblePaths)); // `possiblePaths` contains the error message - // return newest save file path that is valid - var byMostRecent = possiblePaths.OrderByDescending(File.GetLastWriteTimeUtc); - var saves = byMostRecent.Select(SaveUtil.GetSaveFile); - return saves.FirstOrDefault(z => z?.ChecksumsValid == true); + if (token.IsCancellationRequested) + return null; + + foreach (var path in possiblePaths.OrderByDescending(File.GetLastWriteTimeUtc)) + { + if (token.IsCancellationRequested) + break; + + var save = SaveUtil.GetSaveFile(path); + if (save?.ChecksumsValid == true) + return save; + } + + return null; } /// @@ -119,9 +175,10 @@ public static IEnumerable GetSaveFiles(IReadOnlyList drives, b if (!result) yield break; - var byMostRecent = possiblePaths.OrderByDescending(File.GetLastWriteTimeUtc); - foreach (var s in byMostRecent) + foreach (var s in possiblePaths.OrderByDescending(File.GetLastWriteTimeUtc)) { + if (token.IsCancellationRequested) + yield break; if (SaveUtil.TryGetSaveFile(s, out var sav)) yield return sav; } @@ -129,15 +186,27 @@ public static IEnumerable GetSaveFiles(IReadOnlyList drives, b public static IEnumerable GetFoldersToCheck(IReadOnlyList drives, IEnumerable extra, CancellationToken token) { - var foldersToCheck = extra.Where(f => !string.IsNullOrWhiteSpace(f)).Concat(CustomBackupPaths); + if (token.IsCancellationRequested) + return []; - string? path3DS = Path.GetPathRoot(Get3DSLocation(drives)); + var foldersToCheck = new List(); + foreach (var folder in extra) + { + if (!string.IsNullOrWhiteSpace(folder)) + foldersToCheck.Add(folder); + } + + foldersToCheck.AddRange(CustomBackupPaths); + string? path3DS = Path.GetPathRoot(Get3DSLocation(drives, true, token)); if (!string.IsNullOrEmpty(path3DS)) // check for Homebrew/CFW backups - foldersToCheck = foldersToCheck.Concat(Get3DSBackupPaths(path3DS)); + foldersToCheck.AddRange(Get3DSBackupPaths(path3DS)); - string? pathNX = Path.GetPathRoot(GetSwitchLocation(drives)); + if (token.IsCancellationRequested) + return foldersToCheck; + + string? pathNX = Path.GetPathRoot(GetSwitchLocation(drives, true, token)); if (!string.IsNullOrEmpty(pathNX)) // check for Homebrew/CFW backups - foldersToCheck = foldersToCheck.Concat(GetSwitchBackupPaths(pathNX)); + foldersToCheck.AddRange(GetSwitchBackupPaths(pathNX)); return foldersToCheck; } diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs index 305e412c8..4d87511a1 100644 --- a/PKHeX.WinForms/MainWindow/Main.cs +++ b/PKHeX.WinForms/MainWindow/Main.cs @@ -1389,7 +1389,7 @@ private void ClickSaveFileName(object sender, EventArgs e) { try { - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); if (!SaveFinder.TryDetectSaveFile(cts.Token, out var sav)) return; diff --git a/PKHeX.WinForms/Subforms/SAV_FolderList.cs b/PKHeX.WinForms/Subforms/SAV_FolderList.cs index c6210967d..0705bfcae 100644 --- a/PKHeX.WinForms/Subforms/SAV_FolderList.cs +++ b/PKHeX.WinForms/Subforms/SAV_FolderList.cs @@ -29,7 +29,7 @@ public SAV_FolderList(Action openSaveFile) var backups = Main.BackupPath; var drives = Environment.GetLogicalDrives(); - Paths = GetPathList(drives, backups); + Paths = GetPathList(drives, backups, cts.Token); components ??= new System.ComponentModel.Container(); dgDataRecent.ContextMenuStrip = GetContextMenu(dgDataRecent); @@ -80,12 +80,13 @@ private void OnKeyDown(object sender, KeyEventArgs e) Close(); } - private static List GetPathList(IReadOnlyList drives, string backupPath) + private static List GetPathList(IReadOnlyList drives, string backupPath, + CancellationToken token) { List locs = [ new CustomFolderPath(backupPath, DisplayText: "PKHeX Backups"), - ..GetUserPaths(), ..GetPaths3DS(drives), ..GetPathsSwitch(drives), + ..GetUserPaths(), ..GetPaths3DS(drives, token), ..GetPathsSwitch(drives, token), ]; var filtered = locs .DistinctBy(z => z.Path) @@ -129,9 +130,9 @@ private static IEnumerable GetUserPaths() return paths.Select(x => new CustomFolderPath(x, FolderPathGroup.Custom)); } - private static IEnumerable GetPaths3DS(IEnumerable drives) + private static IEnumerable GetPaths3DS(IEnumerable drives, CancellationToken token) { - var path3DS = SaveFinder.Get3DSLocation(drives); + var path3DS = SaveFinder.Get3DSLocation(drives, true, token); if (path3DS is null) return []; @@ -143,9 +144,9 @@ private static IEnumerable GetPaths3DS(IEnumerable dri return paths.Select(z => new CustomFolderPath(z, FolderPathGroup.Nintendo3DS)); } - private static IEnumerable GetPathsSwitch(IEnumerable drives) + private static IEnumerable GetPathsSwitch(IEnumerable drives, CancellationToken token) { - var pathNX = SaveFinder.GetSwitchLocation(drives); + var pathNX = SaveFinder.GetSwitchLocation(drives, true, token); if (pathNX is null) return []; diff --git a/PKHeX.WinForms/Util/WinFormsUtil.cs b/PKHeX.WinForms/Util/WinFormsUtil.cs index fc2cbe7b8..54eb91b9d 100644 --- a/PKHeX.WinForms/Util/WinFormsUtil.cs +++ b/PKHeX.WinForms/Util/WinFormsUtil.cs @@ -338,7 +338,7 @@ public static bool OpenSAVPKMDialog(IEnumerable extensions, [NotNullWhen { try { - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sav = SaveFinder.FindMostRecentSaveFile(cts.Token); return sav?.Metadata.FilePath; }