From 44f2d8a4481d686aea94d6d480cfc0e17ec7351b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 18:48:37 +0900 Subject: [PATCH] Allow game folder migration to fail gracefully when cleanup cannot completely succeed --- osu.Game.Tournament/IO/TournamentStorage.cs | 4 ++- osu.Game/IO/MigratableStorage.cs | 30 +++++++++++----- osu.Game/IO/OsuStorage.cs | 7 ++-- osu.Game/OsuGameBase.cs | 9 ++--- .../Maintenance/MigrationRunScreen.cs | 36 ++++++++++++++++--- 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 347d368a04..b4859d0c91 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tournament.IO public IEnumerable ListTournaments() => AllTournaments.GetDirectories(string.Empty); - public override void Migrate(Storage newStorage) + public override bool Migrate(Storage newStorage) { // this migration only happens once on moving to the per-tournament storage system. // listed files are those known at that point in time. @@ -94,6 +94,8 @@ namespace osu.Game.Tournament.IO ChangeTargetStorage(newStorage); storageConfig.SetValue(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); + + return true; } private void moveFileIfExists(string file, DirectoryInfo destination) diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 1b76725b04..e478144294 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -33,7 +33,8 @@ namespace osu.Game.IO /// A general purpose migration method to move the storage to a different location. /// The target storage of the migration. /// - public virtual void Migrate(Storage newStorage) + /// Whether cleanup could complete. + public virtual bool Migrate(Storage newStorage) { var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newStorage.GetFullPath(".")); @@ -57,17 +58,20 @@ namespace osu.Game.IO CopyRecursive(source, destination); ChangeTargetStorage(newStorage); - DeleteRecursive(source); + + return DeleteRecursive(source); } - protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) + protected bool DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) { + bool allFilesDeleted = true; + foreach (System.IO.FileInfo fi in target.GetFiles()) { if (topLevelExcludes && IgnoreFiles.Contains(fi.Name)) continue; - AttemptOperation(() => fi.Delete()); + allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false); } foreach (DirectoryInfo dir in target.GetDirectories()) @@ -75,11 +79,13 @@ namespace osu.Game.IO if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name)) continue; - AttemptOperation(() => dir.Delete(true)); + allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false); } if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - AttemptOperation(target.Delete); + allFilesDeleted &= AttemptOperation(target.Delete, throwOnFailure: false); + + return allFilesDeleted; } protected void CopyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) @@ -110,19 +116,25 @@ namespace osu.Game.IO /// /// The action to perform. /// The number of attempts (250ms wait between each). - protected static void AttemptOperation(Action action, int attempts = 10) + /// Whether to throw an exception on failure. If false, will silently fail. + protected static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true) { while (true) { try { action(); - return; + return true; } catch (Exception) { if (attempts-- == 0) - throw; + { + if (throwOnFailure) + throw; + + return false; + } } Thread.Sleep(250); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 802c71e363..6e7cb545e3 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -113,11 +113,14 @@ namespace osu.Game.IO } } - public override void Migrate(Storage newStorage) + public override bool Migrate(Storage newStorage) { - base.Migrate(newStorage); + bool cleanupSucceeded = base.Migrate(newStorage); + storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath(".")); storageConfig.Save(); + + return cleanupSucceeded; } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5b2eb5607a..0b2644d5ba 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -413,7 +413,7 @@ namespace osu.Game Scheduler.AddDelayed(GracefullyExit, 2000); } - public void Migrate(string path) + public bool Migrate(string path) { Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); @@ -432,14 +432,15 @@ namespace osu.Game readyToRun.Wait(); - (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + + Logger.Log(@"Migration complete!"); + return cleanupSucceded != false; } finally { realmBlocker?.Dispose(); } - - Logger.Log(@"Migration complete!"); } protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index adb347e7b8..fb7ff0dbd1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -4,13 +4,16 @@ using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osuTK; @@ -23,6 +26,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [Resolved(canBeNull: true)] private OsuGame game { get; set; } + [Resolved] + private NotificationOverlay notifications { get; set; } + + [Resolved] + private Storage storage { get; set; } + + [Resolved] + private GameHost host { get; set; } + public override bool AllowBackButton => false; public override bool AllowExternalScreenChange => false; @@ -84,17 +96,33 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Beatmap.Value = Beatmap.Default; + var originalStorage = new NativeStorage(storage.GetFullPath(string.Empty), host); + migrationTask = Task.Run(PerformMigration) - .ContinueWith(t => + .ContinueWith(task => { - if (t.IsFaulted) - Logger.Error(t.Exception, $"Error during migration: {t.Exception?.Message}"); + if (task.IsFaulted) + { + Logger.Error(task.Exception, $"Error during migration: {task.Exception?.Message}"); + } + else if (!task.GetResultSafely()) + { + notifications.Post(new SimpleNotification + { + Text = "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.", + Activated = () => + { + originalStorage.PresentExternally(); + return true; + } + }); + } Schedule(this.Exit); }); } - protected virtual void PerformMigration() => game?.Migrate(destination.FullName); + protected virtual bool PerformMigration() => game?.Migrate(destination.FullName) != false; public override void OnEntering(IScreen last) {