From 2d970dde75e2dc043e924102ddd8468042df4794 Mon Sep 17 00:00:00 2001 From: Kurt Date: Thu, 13 Aug 2026 23:02:48 -0500 Subject: [PATCH] SaveFinder: better timeout handling Pass CancellationToken thru and better scan drive types to see if we bother trying to access them. Network drives and CD drives are now disallowed, which should lessen the amount of hang-hiccups on startup for some users' hardware setup. --- PKHeX.Core/Saves/Util/SaveFinder.cs | 113 +++++++++++++++++----- PKHeX.WinForms/MainWindow/Main.cs | 2 +- PKHeX.WinForms/Subforms/SAV_FolderList.cs | 15 +-- PKHeX.WinForms/Util/WinFormsUtil.cs | 2 +- 4 files changed, 101 insertions(+), 31 deletions(-) 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; }