Add cancellation to savefile detection calls

5s timeout on detection, roughly
This commit is contained in:
Kurt 2025-04-06 22:25:37 -05:00
parent 56ab067ad9
commit 5ab6dbc0ac
7 changed files with 71 additions and 40 deletions

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
namespace PKHeX.Core;
@ -75,14 +76,14 @@ public void ReadTemplateIfNoEntity(string path)
private static SaveFile? ReadSettingsDefinedPKM(IStartupSettings startup, PKM pk) => startup.AutoLoadSaveOnStartup switch
{
AutoLoadSetting.RecentBackup => SaveFinder.DetectSaveFiles().FirstOrDefault(z => z.IsCompatiblePKM(pk)),
AutoLoadSetting.RecentBackup => SaveFinder.DetectSaveFiles(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token).FirstOrDefault(z => z.IsCompatiblePKM(pk)),
AutoLoadSetting.LastLoaded => GetMostRecentlyLoaded(startup.RecentlyLoaded).FirstOrDefault(z => z.IsCompatiblePKM(pk)),
_ => null,
};
private static SaveFile? ReadSettingsAnyPKM(IStartupSettings startup) => startup.AutoLoadSaveOnStartup switch
{
AutoLoadSetting.RecentBackup => SaveFinder.DetectSaveFiles().FirstOrDefault(),
AutoLoadSetting.RecentBackup => SaveFinder.DetectSaveFiles(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token).FirstOrDefault(),
AutoLoadSetting.LastLoaded => GetMostRecentlyLoaded(startup.RecentlyLoaded).FirstOrDefault(),
_ => null,
};

View File

@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
namespace PKHeX.Core;
@ -76,21 +77,23 @@ public static IEnumerable<string> GetSwitchBackupPaths(string root)
/// Finds a compatible save file that was most recently saved (by file write time).
/// </summary>
/// <param name="drives">List of drives on the host machine.</param>
/// <param name="token">Cancellation token to cancel the operation.</param>
/// <param name="extra">Paths to check in addition to the default paths</param>
/// <returns>Reference to a valid save file, if any.</returns>
public static SaveFile? FindMostRecentSaveFile(IReadOnlyList<string> drives, params string[] extra)
=> FindMostRecentSaveFile(drives, (IEnumerable<string>)extra);
public static SaveFile? FindMostRecentSaveFile(IReadOnlyList<string> drives, CancellationToken token, params string[] extra)
=> FindMostRecentSaveFile(drives, extra, token);
/// <summary>
/// Finds a compatible save file that was most recently saved (by file write time).
/// </summary>
/// <param name="drives">List of drives on the host machine.</param>
/// <param name="extra">Paths to check in addition to the default paths</param>
/// <param name="token">Cancellation token to cancel the operation.</param>
/// <returns>Reference to a valid save file, if any.</returns>
public static SaveFile? FindMostRecentSaveFile(IReadOnlyList<string> drives, IEnumerable<string> extra)
public static SaveFile? FindMostRecentSaveFile(IReadOnlyList<string> drives, IEnumerable<string> extra, CancellationToken token)
{
var foldersToCheck = GetFoldersToCheck(drives, extra);
var result = GetSaveFilePathsFromFolders(foldersToCheck, true, out var possiblePaths);
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
@ -107,11 +110,12 @@ public static IEnumerable<string> GetSwitchBackupPaths(string root)
/// <param name="detect">Detect save files stored in common SD card homebrew locations.</param>
/// <param name="extra">Paths to check in addition to the default paths</param>
/// <param name="ignoreBackups">Option to ignore backup files.</param>
/// <param name="token">Cancellation token to cancel the operation.</param>
/// <returns>Valid save files, if any.</returns>
public static IEnumerable<SaveFile> GetSaveFiles(IReadOnlyList<string> drives, bool detect, IEnumerable<string> extra, bool ignoreBackups)
public static IEnumerable<SaveFile> GetSaveFiles(IReadOnlyList<string> drives, bool detect, IEnumerable<string> extra, bool ignoreBackups, CancellationToken token)
{
var paths = detect ? GetFoldersToCheck(drives, extra) : extra;
var result = GetSaveFilePathsFromFolders(paths, ignoreBackups, out var possiblePaths);
var paths = detect ? GetFoldersToCheck(drives, extra, token) : extra;
var result = GetSaveFilePathsFromFolders(paths, ignoreBackups, out var possiblePaths, token);
if (!result)
yield break;
@ -124,7 +128,7 @@ public static IEnumerable<SaveFile> GetSaveFiles(IReadOnlyList<string> drives, b
}
}
public static IEnumerable<string> GetFoldersToCheck(IReadOnlyList<string> drives, IEnumerable<string> extra)
public static IEnumerable<string> GetFoldersToCheck(IReadOnlyList<string> drives, IEnumerable<string> extra, CancellationToken token)
{
var foldersToCheck = extra.Where(f => !string.IsNullOrWhiteSpace(f)).Concat(CustomBackupPaths);
@ -139,12 +143,15 @@ public static IEnumerable<string> GetFoldersToCheck(IReadOnlyList<string> drives
return foldersToCheck;
}
private static bool GetSaveFilePathsFromFolders(IEnumerable<string> foldersToCheck, bool ignoreBackups, out IEnumerable<string> possible)
private static bool GetSaveFilePathsFromFolders(IEnumerable<string> foldersToCheck, bool ignoreBackups, out IEnumerable<string> possible, CancellationToken token)
{
var possiblePaths = new List<string>();
foreach (var folder in foldersToCheck)
{
if (!SaveUtil.GetSavesFromFolder(folder, true, out IEnumerable<string> files, ignoreBackups))
if (token.IsCancellationRequested)
break;
if (!SaveUtil.GetSavesFromFolder(token, folder, true, out IEnumerable<string> files, ignoreBackups))
{
if (files is not string[] msg) // should always return string[]
continue;
@ -159,23 +166,25 @@ private static bool GetSaveFilePathsFromFolders(IEnumerable<string> foldersToChe
return true;
}
/// <inheritdoc cref="FindMostRecentSaveFile(IReadOnlyList{string},string[])"/>
public static SaveFile? FindMostRecentSaveFile() => FindMostRecentSaveFile(Environment.GetLogicalDrives(), CustomBackupPaths);
/// <inheritdoc cref="FindMostRecentSaveFile(IReadOnlyList{string},CancellationToken,string[])"/>
public static SaveFile? FindMostRecentSaveFile(CancellationToken token) => FindMostRecentSaveFile(DriveList, CustomBackupPaths, token);
/// <inheritdoc cref="GetSaveFiles"/>
public static IEnumerable<SaveFile> DetectSaveFiles() => GetSaveFiles(Environment.GetLogicalDrives(), true, CustomBackupPaths, true);
public static IEnumerable<SaveFile> DetectSaveFiles(CancellationToken token) => GetSaveFiles(DriveList, true, CustomBackupPaths, true, token);
/// <returns>
/// True if a valid save file was found, false otherwise.
/// </returns>
/// <inheritdoc cref="FindMostRecentSaveFile(IReadOnlyList{string},string[])"/>
public static bool TryDetectSaveFile([NotNullWhen(true)] out SaveFile? sav) => TryDetectSaveFile(Environment.GetLogicalDrives(), out sav);
/// <inheritdoc cref="FindMostRecentSaveFile(IReadOnlyList{string},CancellationToken,string[])"/>
public static bool TryDetectSaveFile(CancellationToken token, [NotNullWhen(true)] out SaveFile? sav) => TryDetectSaveFile(token, DriveList, out sav);
/// <inheritdoc cref="TryDetectSaveFile(out SaveFile)"/>
public static bool TryDetectSaveFile(IReadOnlyList<string> drives, [NotNullWhen(true)] out SaveFile? sav)
/// <inheritdoc cref="TryDetectSaveFile(CancellationToken, out SaveFile)"/>
public static bool TryDetectSaveFile(CancellationToken token, IReadOnlyList<string> drives, [NotNullWhen(true)] out SaveFile? sav)
{
sav = FindMostRecentSaveFile(drives, CustomBackupPaths);
sav = FindMostRecentSaveFile(drives, CustomBackupPaths, token);
var path = sav?.Metadata.FilePath;
return File.Exists(path);
}
private static string[] DriveList => Environment.GetLogicalDrives();
}

View File

@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using static System.Buffers.Binary.BinaryPrimitives;
using static PKHeX.Core.MessageStrings;
using static PKHeX.Core.GameVersion;
@ -848,12 +849,13 @@ public static SaveFile GetBlankSAV(EntityContext context, string trainerName, La
/// <summary>
/// Retrieves possible save file paths from the provided <see cref="folderPath"/>.
/// </summary>
/// <param name="token">Cancellation token to cancel the operation.</param>
/// <param name="folderPath">Folder to look within</param>
/// <param name="deep">Search all subfolders</param>
/// <param name="result">If this function returns true, full path of all <see cref="SaveFile"/> that match criteria. If this function returns false, the error message, or null if the directory could not be found</param>
/// <param name="ignoreBackups">Option to ignore files with backup names and extensions</param>
/// <returns>Boolean indicating if the operation was successful.</returns>
public static bool GetSavesFromFolder(string folderPath, bool deep, out IEnumerable<string> result, bool ignoreBackups = true)
public static bool GetSavesFromFolder(CancellationToken token, string folderPath, bool deep, out IEnumerable<string> result, bool ignoreBackups = true)
{
if (!Directory.Exists(folderPath))
{
@ -865,7 +867,7 @@ public static bool GetSavesFromFolder(string folderPath, bool deep, out IEnumera
var searchOption = deep ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var files = Directory.EnumerateFiles(folderPath, "*", searchOption)
.IterateSafe(log: z => System.Diagnostics.Debug.WriteLine(z));
result = FilterSaveFiles(ignoreBackups, files);
result = FilterSaveFiles(token, ignoreBackups, files);
return true;
}
catch (Exception ex)
@ -880,10 +882,13 @@ public static bool GetSavesFromFolder(string folderPath, bool deep, out IEnumera
}
}
private static IEnumerable<string> FilterSaveFiles(bool ignoreBackups, IEnumerable<string> files)
private static IEnumerable<string> FilterSaveFiles(CancellationToken token, bool ignoreBackups, IEnumerable<string> files)
{
foreach (var file in files)
{
if (token.IsCancellationRequested)
yield break;
if (ignoreBackups && IsBackup(file))
continue;

View File

@ -7,6 +7,7 @@
using System.Linq;
using System.Media;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
@ -1343,7 +1344,8 @@ private void ClickSaveFileName(object sender, EventArgs e)
{
try
{
if (!SaveFinder.TryDetectSaveFile(out var sav))
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
if (!SaveFinder.TryDetectSaveFile(cts.Token, out var sav))
return;
var path = sav.Metadata.FilePath!;

View File

@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using PKHeX.Core;
@ -39,10 +40,12 @@ public partial class SAV_Database : Form
private readonly string Viewed;
private const int MAXFORMAT = PKX.Generation;
private readonly SummaryPreviewer ShowSet = new();
private CancellationTokenSource cts = new();
public SAV_Database(PKMEditor f1, SAVEditor saveditor)
{
InitializeComponent();
FormClosing += (_, _) => cts.Cancel();
WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage);
UC_Builder = new EntityInstructionBuilder(() => f1.PreparePKM())
{
@ -123,15 +126,16 @@ public SAV_Database(PKMEditor f1, SAVEditor saveditor)
// Load Data
B_Search.Enabled = false;
L_Count.Text = "Loading...";
var task = new Task(LoadDatabase);
var token = cts.Token;
var task = new Task(() => LoadDatabase(token), cts.Token);
task.ContinueWith(z =>
{
if (!z.IsFaulted)
if (token.IsCancellationRequested || !z.IsFaulted)
return;
Invoke((MethodInvoker)(() => L_Count.Text = "Failed."));
if (z.Exception is null)
return;
WinFormsUtil.Error("Loading database failed.", z.Exception.InnerException ?? new Exception(z.Exception.Message));
WinFormsUtil.Error("Loading database failed.", z.Exception.InnerException ?? z.Exception.GetBaseException());
});
task.Start();
@ -360,7 +364,7 @@ private sealed class SearchFolderDetail(string path, bool ignoreBackupFiles)
public bool IgnoreBackupFiles { get; } = ignoreBackupFiles;
}
private void LoadDatabase()
private void LoadDatabase(CancellationToken token)
{
var settings = Main.Settings;
var otherPaths = new List<SearchFolderDetail>();
@ -369,7 +373,9 @@ private void LoadDatabase()
if (settings.EntityDb.SearchBackups)
otherPaths.Add(new SearchFolderDetail(Main.BackupPath, false));
RawDB = LoadPKMSaves(DatabasePath, SAV, otherPaths, settings.EntityDb.SearchExtraSavesDeep);
RawDB = LoadEntitiesFromFolder(DatabasePath, SAV, otherPaths, settings.EntityDb.SearchExtraSavesDeep, token);
if (token.IsCancellationRequested)
return;
// Load stats for pk who do not have any
foreach (var entry in RawDB)
@ -381,22 +387,24 @@ private void LoadDatabase()
try
{
while (!IsHandleCreated) { }
if (cts.Token.IsCancellationRequested)
return;
BeginInvoke(new MethodInvoker(() => SetResults(RawDB)));
}
catch { /* Window Closed? */ }
}
private static List<SlotCache> LoadPKMSaves(string pkmdb, SaveFile sav, List<SearchFolderDetail> otherPaths, bool otherDeep)
private static List<SlotCache> LoadEntitiesFromFolder(string databaseFolder, SaveFile sav, List<SearchFolderDetail> otherPaths, bool otherDeep, CancellationToken token)
{
var dbTemp = new ConcurrentBag<SlotCache>();
var extensions = new HashSet<string>(EntityFileExtension.GetExtensionsAll().Select(z => $".{z}"));
var files = Directory.EnumerateFiles(pkmdb, "*", SearchOption.AllDirectories);
var files = Directory.EnumerateFiles(databaseFolder, "*", SearchOption.AllDirectories);
Parallel.ForEach(files, file => SlotInfoLoader.AddFromLocalFile(file, dbTemp, sav, extensions));
foreach (var folder in otherPaths)
{
if (!SaveUtil.GetSavesFromFolder(folder.Path, otherDeep, out IEnumerable<string> paths, folder.IgnoreBackupFiles))
if (!SaveUtil.GetSavesFromFolder(token, folder.Path, otherDeep, out var paths, folder.IgnoreBackupFiles))
continue;
Parallel.ForEach(paths, file => TryAddPKMsFromSaveFilePath(dbTemp, file));

View File

@ -4,6 +4,7 @@
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using PKHeX.Core;
@ -19,23 +20,27 @@ public partial class SAV_FolderList : Form
private readonly SortableBindingList<SavePreview> Recent;
private readonly SortableBindingList<SavePreview> Backup;
private readonly List<Label> TempTranslationLabels = [];
private readonly CancellationTokenSource cts = new(TimeSpan.FromSeconds(20));
public SAV_FolderList(Action<SaveFile> openSaveFile)
{
InitializeComponent();
FormClosing += (_, _) => cts.Cancel();
OpenSaveFile = openSaveFile;
var backups = Main.BackupPath;
var drives = Environment.GetLogicalDrives();
Paths = GetPathList(drives);
Paths = GetPathList(drives, backups);
dgDataRecent.ContextMenuStrip = GetContextMenu(dgDataRecent);
dgDataBackup.ContextMenuStrip = GetContextMenu(dgDataBackup);
dgDataRecent.Sorted += (_, _) => GetFilterText(dgDataRecent);
dgDataBackup.Sorted += (_, _) => GetFilterText(dgDataBackup);
var extra = Paths.Select(z => z.Path).Where(z => z != Main.BackupPath).Distinct();
var backup = SaveFinder.GetSaveFiles(drives, false, [Main.BackupPath], false);
var recent = SaveFinder.GetSaveFiles(drives, false, extra, true).ToList();
var token = cts.Token;
var extra = Paths.Select(z => z.Path).Where(z => z != backups).Distinct();
var backup = SaveFinder.GetSaveFiles(drives, false, [backups], false, token);
var recent = SaveFinder.GetSaveFiles(drives, false, extra, true, token).ToList();
var loaded = Main.Settings.Startup.RecentlyLoaded
.Where(z => recent.All(x => x.Metadata.FilePath != z))
.Where(File.Exists).Select(SaveUtil.GetVariantSAV).OfType<SaveFile>();
@ -76,11 +81,11 @@ public SAV_FolderList(Action<SaveFile> openSaveFile)
CenterToParent();
}
private static List<INamedFolderPath> GetPathList(IReadOnlyList<string> drives)
private static List<INamedFolderPath> GetPathList(IReadOnlyList<string> drives, string backupPath)
{
List<INamedFolderPath> locs =
[
new CustomFolderPath(Main.BackupPath, display: "PKHeX Backups"),
new CustomFolderPath(backupPath, display: "PKHeX Backups"),
..GetUserPaths(), ..GetConsolePaths(drives), ..GetSwitchPaths(drives),
];
var filtered = locs

View File

@ -291,7 +291,8 @@ public static bool OpenSAVPKMDialog(IEnumerable<string> extensions, out string?
{
try
{
var sav = SaveFinder.FindMostRecentSaveFile();
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var sav = SaveFinder.FindMostRecentSaveFile(cts.Token);
return sav?.Metadata.FilePath;
}
catch (Exception ex)