mirror of https://github.com/ppy/osu
Merge pull request #28743 from smallketchup82/velopack
Migrate update framework to Velopack
This commit is contained in:
commit
1017a124ab
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,37 +102,15 @@ 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();
|
||||
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.RestartAppWhenExited();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
|
|
@ -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) =>
|
||||
VelopackApp
|
||||
.Build()
|
||||
.WithFirstRun(v =>
|
||||
{
|
||||
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();
|
||||
});
|
||||
if (OperatingSystem.IsWindows()) WindowsAssociationManager.InstallAssociations();
|
||||
}).Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,180 +0,0 @@
|
|||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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");
|
||||
|
||||
/// <summary>
|
||||
/// Whether an update has been downloaded but not yet applied.
|
||||
/// </summary>
|
||||
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<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
||||
|
||||
private async Task<bool> 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
||||
|
||||
private async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,9 +23,9 @@
|
|||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.11.1" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.598-g933b2ab" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
|
|
@ -135,11 +135,6 @@ public static class NotificationsStrings
|
|||
/// </summary>
|
||||
public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update...");
|
||||
|
||||
/// <summary>
|
||||
/// "Installing update..."
|
||||
/// </summary>
|
||||
public static LocalisableString InstallingUpdate => new TranslatableString(getKey(@"installing_update"), @"Installing update...");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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.
|
|
@ -176,12 +176,6 @@ public void StartDownload()
|
|||
Text = NotificationsStrings.DownloadingUpdate;
|
||||
}
|
||||
|
||||
public void StartInstall()
|
||||
{
|
||||
Progress = 0;
|
||||
Text = NotificationsStrings.InstallingUpdate;
|
||||
}
|
||||
|
||||
public void FailDownload()
|
||||
{
|
||||
State = ProgressNotificationState.Cancelled;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -1061,5 +1061,6 @@ private void load()
|
|||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unplayed/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unproxy/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unranked/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=velopack/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Welford_0027s/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zoomable/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
|
Loading…
Reference in New Issue