// 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 bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying; 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() { // whether to check again in 30 minutes. generally only if there's an error or no update was found (yet). bool scheduleRecheck = false; try { // Avoid any kind of update checking while gameplay is running. if (isInGameplay) { scheduleRecheck = true; return true; } // TODO: we should probably be checking if there's a more recent update, rather than shortcutting here. // Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975). if (pendingUpdate != null) { // If there is an update pending restart, show the notification to restart again. notificationOverlay.Post(new UpdateApplicationCompleteNotification { Activated = () => { Task.Run(restartToApplyUpdate); return true; } }); return true; } pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); // No update is available. We'll check again later. if (pendingUpdate == null) { scheduleRecheck = true; return false; } // An update is found, let's notify the user and start downloading it. UpdateProgressNotification notification = new UpdateProgressNotification { CompletionClickAction = () => { Task.Run(restartToApplyUpdate); return true; }, }; runOutsideOfGameplay(() => notificationOverlay.Post(notification)); notification.StartDownload(); try { await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false); runOutsideOfGameplay(() => notification.State = ProgressNotificationState.Completed); } catch (Exception e) { // In the case of an error, a separate notification will be displayed. scheduleRecheck = true; 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) { Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30); } } return true; } private void runOutsideOfGameplay(Action action) { if (isInGameplay) { Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000); return; } action(); } private async Task restartToApplyUpdate() { await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false); Schedule(() => game.AttemptExit()); } } }