diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index a235913ef3aa..ffab7dd86da2 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -80,7 +80,7 @@ public override void SetHost(GameHost host) host.Window.CursorState |= CursorState.Hidden; } - protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 3e06dad4c54b..c75a3f0a1af5 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -2,10 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.Versioning; +using System.Threading.Tasks; using Microsoft.Win32; using osu.Desktop.Performance; using osu.Desktop.Security; @@ -102,35 +102,13 @@ protected override UpdateManager CreateUpdateManager() if (!string.IsNullOrEmpty(packageManaged)) return new NoActionUpdateManager(); - switch (RuntimeInfo.OS) - { - case RuntimeInfo.Platform.Windows: - Debug.Assert(OperatingSystem.IsWindows()); - - return new SquirrelUpdateManager(); - - default: - return new SimpleUpdateManager(); - } + return new VelopackUpdateManager(); } public override bool RestartAppWhenExited() { - switch (RuntimeInfo.OS) - { - case RuntimeInfo.Platform.Windows: - Debug.Assert(OperatingSystem.IsWindows()); - - // Of note, this is an async method in squirrel that adds an arbitrary delay before returning - // likely to ensure the external process is in a good state. - // - // We're not waiting on that here, but the outro playing before the actual exit should be enough - // to cover this. - Squirrel.UpdateManager.RestartAppWhenExited().FireAndForget(); - return true; - } - - return base.RestartAppWhenExited(); + Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget(); + return true; } protected override void LoadComplete() diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 23e56cdce9c6..510366381501 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -3,7 +3,6 @@ using System; using System.IO; -using System.Runtime.Versioning; using osu.Desktop.LegacyIpc; using osu.Desktop.Windows; using osu.Framework; @@ -14,7 +13,7 @@ using osu.Game.IPC; using osu.Game.Tournament; using SDL; -using Squirrel; +using Velopack; namespace osu.Desktop { @@ -31,19 +30,11 @@ public static class Program [STAThread] public static void Main(string[] args) { - /* - * WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK! - * - * Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it. - * To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit, - * namely by checking loaded assemblies: - * https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32 - * - * If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded - - * the app will then do completely broken things like: - * - not creating system shortcuts (as the logic is if'd out if "running tests") - * - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests") - */ + // IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else. + // This has bitten us in the rear before (bricked updater), and although the underlying issue from + // last time has been fixed, let's not tempt fate. + setupVelopack(); + if (OperatingSystem.IsWindows()) { var windowsVersion = Environment.OSVersion.Version; @@ -66,8 +57,6 @@ public static void Main(string[] args) return; } } - - setupSquirrel(); } // NVIDIA profiles are based on the executable name of a process. @@ -177,32 +166,14 @@ private static bool trySendIPCMessage(IIpcHost host, string cwd, string[] args) return false; } - [SupportedOSPlatform("windows")] - private static void setupSquirrel() + private static void setupVelopack() { - SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) => - { - tools.CreateShortcutForThisExe(); - tools.CreateUninstallerRegistryEntry(); - WindowsAssociationManager.InstallAssociations(); - }, onAppUpdate: (_, tools) => - { - tools.CreateUninstallerRegistryEntry(); - WindowsAssociationManager.UpdateAssociations(); - }, onAppUninstall: (_, tools) => - { - tools.RemoveShortcutForThisExe(); - tools.RemoveUninstallerRegistryEntry(); - WindowsAssociationManager.UninstallAssociations(); - }, onEveryRun: (_, _, _) => - { - // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently - // causes the right-click context menu to function incorrectly. - // - // This may turn out to be non-required after an alternative solution is implemented. - // see https://github.com/clowd/Clowd.Squirrel/issues/24 - // tools.SetProcessAppUserModelId(); - }); + VelopackApp + .Build() + .WithFirstRun(v => + { + if (OperatingSystem.IsWindows()) WindowsAssociationManager.InstallAssociations(); + }).Run(); } } } diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs deleted file mode 100644 index dba157a6e951..000000000000 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Runtime.Versioning; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Logging; -using osu.Game; -using osu.Game.Overlays; -using osu.Game.Overlays.Notifications; -using osu.Game.Screens.Play; -using Squirrel.SimpleSplat; -using Squirrel.Sources; -using LogLevel = Squirrel.SimpleSplat.LogLevel; -using UpdateManager = osu.Game.Updater.UpdateManager; - -namespace osu.Desktop.Updater -{ - [SupportedOSPlatform("windows")] - public partial class SquirrelUpdateManager : UpdateManager - { - private Squirrel.UpdateManager? updateManager; - private INotificationOverlay notificationOverlay = null!; - - public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited(); - - private static readonly Logger logger = Logger.GetLogger("updater"); - - /// - /// Whether an update has been downloaded but not yet applied. - /// - private bool updatePending; - - private readonly SquirrelLogger squirrelLogger = new SquirrelLogger(); - - [Resolved] - private OsuGameBase game { get; set; } = null!; - - [Resolved] - private ILocalUserPlayInfo? localUserInfo { get; set; } - - [BackgroundDependencyLoader] - private void load(INotificationOverlay notifications) - { - notificationOverlay = notifications; - - SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger)); - } - - protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); - - private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification? notification = null) - { - // should we schedule a retry on completion of this check? - bool scheduleRecheck = true; - - const string? github_token = null; // TODO: populate. - - try - { - // Avoid any kind of update checking while gameplay is running. - if (localUserInfo?.IsPlaying.Value == true) - return false; - - updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer"); - - var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); - - if (info.ReleasesToApply.Count == 0) - { - if (updatePending) - { - // the user may have dismissed the completion notice, so show it again. - notificationOverlay.Post(new UpdateApplicationCompleteNotification - { - Activated = () => - { - restartToApplyUpdate(); - return true; - }, - }); - return true; - } - - // no updates available. bail and retry later. - return false; - } - - scheduleRecheck = false; - - if (notification == null) - { - notification = new UpdateProgressNotification - { - CompletionClickAction = restartToApplyUpdate, - }; - - Schedule(() => notificationOverlay.Post(notification)); - } - - notification.StartDownload(); - - try - { - await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false); - - notification.StartInstall(); - - await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false); - - notification.State = ProgressNotificationState.Completed; - updatePending = true; - } - catch (Exception e) - { - if (useDeltaPatching) - { - logger.Add(@"delta patching failed; will attempt full download!"); - - // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959) - // try again without deltas. - await checkForUpdateAsync(false, notification).ConfigureAwait(false); - } - else - { - // In the case of an error, a separate notification will be displayed. - notification.FailDownload(); - Logger.Error(e, @"update failed!"); - } - } - } - catch (Exception) - { - // we'll ignore this and retry later. can be triggered by no internet connection or thread abortion. - scheduleRecheck = true; - } - finally - { - if (scheduleRecheck) - { - // check again in 30 minutes. - Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30); - } - } - - return true; - } - - private bool restartToApplyUpdate() - { - PrepareUpdateAsync() - .ContinueWith(_ => Schedule(() => game.AttemptExit())); - return true; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - updateManager?.Dispose(); - } - - private class SquirrelLogger : ILogger, IDisposable - { - public LogLevel Level { get; set; } = LogLevel.Info; - - public void Write(string message, LogLevel logLevel) - { - if (logLevel < Level) - return; - - logger.Add(message); - } - - public void Dispose() - { - } - } - } -} diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs new file mode 100644 index 000000000000..527892413a3f --- /dev/null +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -0,0 +1,131 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Logging; +using osu.Game; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Play; +using Velopack; +using Velopack.Sources; + +namespace osu.Desktop.Updater +{ + public partial class VelopackUpdateManager : Game.Updater.UpdateManager + { + private readonly UpdateManager updateManager; + private INotificationOverlay notificationOverlay = null!; + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + [Resolved] + private ILocalUserPlayInfo? localUserInfo { get; set; } + + private UpdateInfo? pendingUpdate; + + public VelopackUpdateManager() + { + updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions + { + AllowVersionDowngrade = true, + }); + } + + [BackgroundDependencyLoader] + private void load(INotificationOverlay notifications) + { + notificationOverlay = notifications; + } + + protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); + + private async Task checkForUpdateAsync(UpdateProgressNotification? notification = null) + { + // should we schedule a retry on completion of this check? + bool scheduleRecheck = true; + + try + { + // Avoid any kind of update checking while gameplay is running. + if (localUserInfo?.IsPlaying.Value == true) + return false; + + if (pendingUpdate != null) + { + // If there is an update pending restart, show the notification to restart again. + notificationOverlay.Post(new UpdateApplicationCompleteNotification + { + Activated = () => + { + restartToApplyUpdate(); + return true; + } + }); + return true; + } + + pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); + + // Handle no updates available. + if (pendingUpdate == null) + return false; + + scheduleRecheck = false; + + if (notification == null) + { + notification = new UpdateProgressNotification + { + CompletionClickAction = restartToApplyUpdate, + }; + + Schedule(() => notificationOverlay.Post(notification)); + } + + notification.StartDownload(); + + try + { + await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false); + + notification.State = ProgressNotificationState.Completed; + } + catch (Exception e) + { + // In the case of an error, a separate notification will be displayed. + notification.FailDownload(); + Logger.Error(e, @"update failed!"); + } + } + catch (Exception e) + { + // we'll ignore this and retry later. can be triggered by no internet connection or thread abortion. + scheduleRecheck = true; + Logger.Log($@"update check failed ({e.Message})"); + } + finally + { + if (scheduleRecheck) + { + // check again in 30 minutes. + Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30); + } + } + + return true; + } + + private bool restartToApplyUpdate() + { + // TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665). + // Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart. + updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease); + Schedule(() => game.AttemptExit()); + return true; + } + } +} diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index acb53835a31f..3588317b8a8b 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -23,9 +23,9 @@ - + diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 698fe230b294..d8f768f2d85e 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -135,11 +135,6 @@ public static class NotificationsStrings /// public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update..."); - /// - /// "Installing update..." - /// - public static LocalisableString InstallingUpdate => new TranslatableString(getKey(@"installing_update"), @"Installing update..."); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/MobileUpdateNotifier.cs similarity index 81% rename from osu.Game/Updater/SimpleUpdateManager.cs rename to osu.Game/Updater/MobileUpdateNotifier.cs index 0f9d5b929f96..04b54df3c05d 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/MobileUpdateNotifier.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Runtime.InteropServices; using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; @@ -16,10 +15,10 @@ namespace osu.Game.Updater { /// - /// An update manager that shows notifications if a newer release is detected. + /// An update manager that shows notifications if a newer release is detected for mobile platforms. /// Installation is left up to the user. /// - public partial class SimpleUpdateManager : UpdateManager + public partial class MobileUpdateNotifier : UpdateManager { private string version = null!; @@ -80,19 +79,6 @@ private bool tryGetBestUrl(GitHubRelease release, [NotNullWhen(true)] out string switch (RuntimeInfo.OS) { - case RuntimeInfo.Platform.Windows: - bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal)); - break; - - case RuntimeInfo.Platform.macOS: - string arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "Apple.Silicon" : "Intel"; - bestAsset = release.Assets?.Find(f => f.Name.EndsWith($".app.{arch}.zip", StringComparison.Ordinal)); - break; - - case RuntimeInfo.Platform.Linux: - bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage", StringComparison.Ordinal)); - break; - case RuntimeInfo.Platform.iOS: if (release.Assets?.Exists(f => f.Name.EndsWith(".ipa", StringComparison.Ordinal)) == true) // iOS releases are available via testflight. this link seems to work well enough for now. diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index bcb28d8b14e8..c114e3a8d057 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -176,12 +176,6 @@ public void StartDownload() Text = NotificationsStrings.DownloadingUpdate; } - public void StartInstall() - { - Progress = 0; - Text = NotificationsStrings.InstallingUpdate; - } - public void FailDownload() { State = ProgressNotificationState.Cancelled; diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 502f30215780..2a4f9b87ac6c 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -15,7 +15,7 @@ public partial class OsuGameIOS : OsuGame { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); - protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 2d0b7446f3e1..4a2ef97520d3 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -1061,5 +1061,6 @@ private void load() True True True + True True True