diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs index 66971364c..07b0ebbb2 100644 --- a/src/Ryujinx.Common/ReleaseInformation.cs +++ b/src/Ryujinx.Common/ReleaseInformation.cs @@ -32,59 +32,11 @@ namespace Ryujinx.Common public static string Version => IsValid ? BuildVersion : Assembly.GetEntryAssembly()!.GetCustomAttribute()?.InformationalVersion; - public static string GetChangelogUrl(Version currentVersion, Version newVersion, ReleaseChannels.Channel releaseChannel) => + public static string GetChangelogUrl(Version currentVersion, Version newVersion, string stableUrl) => IsCanaryBuild ? $"https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-{currentVersion}...Canary-{newVersion}" - : GetChangelogForVersion(newVersion, releaseChannel); - - public static string GetChangelogForVersion(Version version, ReleaseChannels.Channel releaseChannel) => - $"https://github.com/{releaseChannel}/releases/{version}"; - - public static async Task GetReleaseChannelsAsync(HttpClient httpClient) - { - ReleaseChannelPair releaseChannelPair = await httpClient.GetFromJsonAsync("https://ryujinx.app/api/release-channels", ReleaseChannelPairContext.Default.ReleaseChannelPair); - return new ReleaseChannels(releaseChannelPair); - } + : stableUrl; } - public readonly struct ReleaseChannels - { - internal ReleaseChannels(ReleaseChannelPair channelPair) - { - Stable = new Channel(channelPair.Stable); - Canary = new Channel(channelPair.Canary); - } - public readonly Channel Stable; - public readonly Channel Canary; - - public readonly struct Channel - { - public Channel(string raw) - { - string[] parts = raw.Split('/'); - Owner = parts[0]; - Repo = parts[1]; - } - - public readonly string Owner; - public readonly string Repo; - - public override string ToString() => $"{Owner}/{Repo}"; - - public string GetLatestReleaseApiUrl() => - $"https://api.github.com/repos/{ToString()}/releases/latest"; - } - } - - [JsonSerializable(typeof(ReleaseChannelPair))] - partial class ReleaseChannelPairContext : JsonSerializerContext; - - class ReleaseChannelPair - { - [JsonPropertyName("stable")] - public string Stable { get; set; } - [JsonPropertyName("canary")] - public string Canary { get; set; } - } } diff --git a/src/Ryujinx/Common/Models/GitLab/GitLabReleaseAssetJsonResponse.cs b/src/Ryujinx/Common/Models/GitLab/GitLabReleaseAssetJsonResponse.cs new file mode 100644 index 000000000..a5b4bb619 --- /dev/null +++ b/src/Ryujinx/Common/Models/GitLab/GitLabReleaseAssetJsonResponse.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Ava.Common.Models.GitLab +{ + public class GitLabReleaseAssetJsonResponse + { + [JsonPropertyName("links")] + public GitLabReleaseAssetLinkJsonResponse[] Links { get; set; } + + public class GitLabReleaseAssetLinkJsonResponse + { + [JsonPropertyName("id")] + public long Id { get; set; } + [JsonPropertyName("name")] + public string AssetName { get; set; } + [JsonPropertyName("url")] + public string Url { get; set; } + } + } +} diff --git a/src/Ryujinx/Common/Models/GitLab/GitLabReleasesJsonResponse.cs b/src/Ryujinx/Common/Models/GitLab/GitLabReleasesJsonResponse.cs new file mode 100644 index 000000000..8d229a5f1 --- /dev/null +++ b/src/Ryujinx/Common/Models/GitLab/GitLabReleasesJsonResponse.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Ava.Common.Models.GitLab +{ + public class GitLabReleasesJsonResponse + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("tag_name")] + public string TagName { get; set; } + + [JsonPropertyName("assets")] + public GitLabReleaseAssetJsonResponse Assets { get; set; } + } + + [JsonSerializable(typeof(GitLabReleasesJsonResponse), GenerationMode = JsonSourceGenerationMode.Metadata)] + public partial class GitLabReleasesJsonSerializerContext : JsonSerializerContext; +} diff --git a/src/Ryujinx/Systems/Updater/Updater.GitHub.cs b/src/Ryujinx/Systems/Updater/Updater.GitHub.cs new file mode 100644 index 000000000..d1738183d --- /dev/null +++ b/src/Ryujinx/Systems/Updater/Updater.GitHub.cs @@ -0,0 +1,193 @@ +using Gommon; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Common.Models.Github; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Common; +using Ryujinx.Common.Helper; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Systems +{ + internal static partial class Updater + { + private static GitHubReleaseChannels.Channel? _currentGitHubReleaseChannel; + + private const string GitHubApiUrl = "https://api.github.com"; + + public static async Task> CheckGitHubVersionAsync(bool showVersionUpToDate = false) + { + if (!Version.TryParse(Program.Version, out Version currentVersion)) + { + Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!"); + + await ContentDialogHelper.CreateWarningDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage], + LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]); + + _running = false; + + return default; + } + + Logger.Info?.Print(LogClass.Application, "Checking for updates from GitHub."); + + // Get latest version number from GitHub API + try + { + using HttpClient jsonClient = ConstructHttpClient(); + + if (_currentGitHubReleaseChannel == null) + { + GitHubReleaseChannels releaseChannels = await GitHubReleaseChannels.GetAsync(jsonClient); + + _currentGitHubReleaseChannel = ReleaseInformation.IsCanaryBuild + ? releaseChannels.Canary + : releaseChannels.Stable; + + _changelogUrlFormat = _currentGitHubReleaseChannel.Value.UrlFormat; + _stableUrlFormat = releaseChannels.Stable.UrlFormat; + } + + string fetchedJson = await jsonClient.GetStringAsync(_currentGitHubReleaseChannel.Value.GetLatestReleaseApiUrl()); + GithubReleasesJsonResponse fetched = JsonHelper.Deserialize(fetchedJson, _ghSerializerContext.GithubReleasesJsonResponse); + _buildVer = fetched.TagName; + + foreach (GithubReleaseAssetJsonResponse asset in fetched.Assets) + { + if (asset.Name.StartsWith("ryujinx") && asset.Name.EndsWith(_platformExt)) + { + _buildUrl = asset.BrowserDownloadUrl; + + if (asset.State != "uploaded") + { + if (showVersionUpToDate) + { + UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], + string.Empty); + + if (userResult is UserResult.Ok) + { + OpenHelper.OpenUrl(_currentGitHubReleaseChannel.Value.GetSpecificReleaseUrl(currentVersion)); + } + } + + Logger.Info?.Print(LogClass.Application, "Up to date."); + + _running = false; + + return default; + } + + break; + } + } + + // If build not done, assume no new update are available. + if (_buildUrl is null) + { + if (showVersionUpToDate) + { + UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], + string.Empty); + + if (userResult is UserResult.Ok) + { + OpenHelper.OpenUrl(_currentGitHubReleaseChannel.Value.GetSpecificReleaseUrl(currentVersion)); + } + } + + Logger.Info?.Print(LogClass.Application, "Up to date."); + + _running = false; + + return default; + } + } + catch (Exception exception) + { + Logger.Error?.Print(LogClass.Application, exception.Message); + + await ContentDialogHelper.CreateErrorDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]); + + _running = false; + + return default; + } + + if (!Version.TryParse(_buildVer, out Version newVersion)) + { + Logger.Error?.Print(LogClass.Application, $"Failed to convert the received {RyujinxApp.FullAppName} version from GitHub!"); + + await ContentDialogHelper.CreateWarningDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage], + LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]); + + _running = false; + + return default; + } + + return (currentVersion, newVersion); + } + } + + public readonly struct GitHubReleaseChannels + { + public static async Task GetAsync(HttpClient httpClient) + { + ReleaseChannelPair releaseChannelPair = await httpClient.GetFromJsonAsync("https://ryujinx.app/api/release-channels", ReleaseChannelPairContext.Default.ReleaseChannelPair); + return new GitHubReleaseChannels(releaseChannelPair); + } + + internal GitHubReleaseChannels(ReleaseChannelPair channelPair) + { + Stable = new Channel(channelPair.Stable); + Canary = new Channel(channelPair.Canary); + } + + public readonly Channel Stable; + public readonly Channel Canary; + + public readonly struct Channel + { + public Channel(string raw) + { + string[] parts = raw.Split('/'); + Owner = parts[0]; + Repo = parts[1]; + } + + public readonly string Owner; + public readonly string Repo; + + public string UrlFormat => $"https://github.com/{ToString()}/releases/{0}"; + + public override string ToString() => $"{Owner}/{Repo}"; + + public string GetLatestReleaseApiUrl() => + $"https://api.github.com/repos/{ToString()}/releases/latest"; + + public string GetSpecificReleaseUrl(Version version) => $"https://github.com/{ToString()}/releases/{version}"; + } + } + + [JsonSerializable(typeof(ReleaseChannelPair))] + partial class ReleaseChannelPairContext : JsonSerializerContext; + + class ReleaseChannelPair + { + [JsonPropertyName("stable")] + public string Stable { get; set; } + [JsonPropertyName("canary")] + public string Canary { get; set; } + } +} diff --git a/src/Ryujinx/Systems/Updater/Updater.GitLab.cs b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs new file mode 100644 index 000000000..4e3169dce --- /dev/null +++ b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs @@ -0,0 +1,152 @@ +using Gommon; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Common.Models.Github; +using Ryujinx.Ava.Common.Models.GitLab; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Common; +using Ryujinx.Common.Helper; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Systems +{ + internal static partial class Updater + { + private static GitLabReleaseChannels.ChannelType _currentGitLabReleaseChannel; + + public static async Task> CheckGitLabVersionAsync(bool showVersionUpToDate = false) + { + if (!Version.TryParse(Program.Version, out Version currentVersion)) + { + Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!"); + + await ContentDialogHelper.CreateWarningDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage], + LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]); + + _running = false; + + return default; + } + + Logger.Info?.Print(LogClass.Application, "Checking for updates from https://git.ryujinx.app."); + + // Get latest version number from GitLab API + try + { + using HttpClient jsonClient = ConstructHttpClient(); + + if (_currentGitLabReleaseChannel == null) + { + GitLabReleaseChannels releaseChannels = await GitLabReleaseChannels.GetAsync(jsonClient); + + _currentGitLabReleaseChannel = ReleaseInformation.IsCanaryBuild + ? releaseChannels.Canary + : releaseChannels.Stable; + + _changelogUrlFormat = _currentGitLabReleaseChannel.UrlFormat; + _stableUrlFormat = releaseChannels.Stable.UrlFormat; + } + + string fetchedJson = await jsonClient.GetStringAsync(_currentGitLabReleaseChannel.GetLatestReleaseApiUrl()); + GitLabReleasesJsonResponse fetched = JsonHelper.Deserialize(fetchedJson, _glSerializerContext.GitLabReleasesJsonResponse); + _buildVer = fetched.TagName; + + foreach (GitLabReleaseAssetJsonResponse.GitLabReleaseAssetLinkJsonResponse asset in fetched.Assets.Links) + { + if (asset.AssetName.StartsWith("ryujinx") && asset.AssetName.EndsWith(_platformExt)) + { + _buildUrl = asset.Url; + break; + } + } + + // If build not done, assume no new update are available. + if (_buildUrl is null) + { + if (showVersionUpToDate) + { + UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], + string.Empty); + + if (userResult is UserResult.Ok) + { + OpenHelper.OpenUrl(_currentGitHubReleaseChannel.Value.GetSpecificReleaseUrl(currentVersion)); + } + } + + Logger.Info?.Print(LogClass.Application, "Up to date."); + + _running = false; + + return default; + } + } + catch (Exception exception) + { + Logger.Error?.Print(LogClass.Application, exception.Message); + + await ContentDialogHelper.CreateErrorDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]); + + _running = false; + + return default; + } + + if (!Version.TryParse(_buildVer, out Version newVersion)) + { + Logger.Error?.Print(LogClass.Application, $"Failed to convert the received {RyujinxApp.FullAppName} version from GitHub!"); + + await ContentDialogHelper.CreateWarningDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage], + LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]); + + _running = false; + + return default; + } + + return (currentVersion, newVersion); + } + + [JsonSerializable(typeof(GitLabReleaseChannels))] + partial class GitLabReleaseChannelPairContext : JsonSerializerContext; + + public class GitLabReleaseChannels + { + public static async Task GetAsync(HttpClient httpClient) + => await httpClient.GetFromJsonAsync("https://git.ryujinx.app/ryubing/ryujinx/-/snippets/1/raw/main/meta.json", GitLabReleaseChannelPairContext.Default.GitLabReleaseChannels); + + [JsonPropertyName("stable")] + public ChannelType Stable { get; set; } + [JsonPropertyName("canary")] + public ChannelType Canary { get; set; } + + public class ChannelType + { + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("group")] + public string Group { get; set; } + + [JsonPropertyName("project")] + public string Project { get; set; } + + public string UrlFormat => $"https://git.ryujinx.app/{ToString()}/-/releases/{{0}}"; + + public override string ToString() => $"{Group}/{Project}"; + + public string GetLatestReleaseApiUrl() => + $"https://git.ryujinx.app/api/v4/{Id}/releases/permalink/latest"; + } + } + } +} diff --git a/src/Ryujinx/Systems/Updater.cs b/src/Ryujinx/Systems/Updater/Updater.cs similarity index 83% rename from src/Ryujinx/Systems/Updater.cs rename to src/Ryujinx/Systems/Updater/Updater.cs index b74b6eaa8..d1fddb25f 100644 --- a/src/Ryujinx/Systems/Updater.cs +++ b/src/Ryujinx/Systems/Updater/Updater.cs @@ -6,6 +6,7 @@ using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Models.Github; +using Ryujinx.Ava.Common.Models.GitLab; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.Utilities; using Ryujinx.Common; @@ -29,13 +30,11 @@ using System.Threading.Tasks; namespace Ryujinx.Ava.Systems { - internal static class Updater + internal static partial class Updater { - private static ReleaseChannels.Channel? _currentReleaseChannel; - - private const string GitHubApiUrl = "https://api.github.com"; - - private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly GithubReleasesJsonSerializerContext _ghSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly GitLabReleasesJsonSerializerContext _glSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory; private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update"); @@ -54,121 +53,8 @@ namespace Ryujinx.Ava.Systems private static readonly string[] _windowsDependencyDirs = []; - public static async Task> CheckVersionAsync(bool showVersionUpToDate = false) - { - if (!Version.TryParse(Program.Version, out Version currentVersion)) - { - Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!"); - - await ContentDialogHelper.CreateWarningDialog( - LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage], - LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]); - - _running = false; - - return default; - } - - Logger.Info?.Print(LogClass.Application, "Checking for updates."); - - // Get latest version number from GitHub API - try - { - using HttpClient jsonClient = ConstructHttpClient(); - - if (_currentReleaseChannel == null) - { - ReleaseChannels releaseChannels = await ReleaseInformation.GetReleaseChannelsAsync(jsonClient); - - _currentReleaseChannel = ReleaseInformation.IsCanaryBuild - ? releaseChannels.Canary - : releaseChannels.Stable; - } - - string fetchedJson = await jsonClient.GetStringAsync(_currentReleaseChannel.Value.GetLatestReleaseApiUrl()); - GithubReleasesJsonResponse fetched = JsonHelper.Deserialize(fetchedJson, _serializerContext.GithubReleasesJsonResponse); - _buildVer = fetched.TagName; - - foreach (GithubReleaseAssetJsonResponse asset in fetched.Assets) - { - if (asset.Name.StartsWith("ryujinx") && asset.Name.EndsWith(_platformExt)) - { - _buildUrl = asset.BrowserDownloadUrl; - - if (asset.State != "uploaded") - { - if (showVersionUpToDate) - { - UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( - LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], - string.Empty); - - if (userResult is UserResult.Ok) - { - OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion, _currentReleaseChannel.Value)); - } - } - - Logger.Info?.Print(LogClass.Application, "Up to date."); - - _running = false; - - return default; - } - - break; - } - } - - // If build not done, assume no new update are available. - if (_buildUrl is null) - { - if (showVersionUpToDate) - { - UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( - LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], - string.Empty); - - if (userResult is UserResult.Ok) - { - OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion, _currentReleaseChannel.Value)); - } - } - - Logger.Info?.Print(LogClass.Application, "Up to date."); - - _running = false; - - return default; - } - } - catch (Exception exception) - { - Logger.Error?.Print(LogClass.Application, exception.Message); - - await ContentDialogHelper.CreateErrorDialog( - LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]); - - _running = false; - - return default; - } - - if (!Version.TryParse(_buildVer, out Version newVersion)) - { - Logger.Error?.Print(LogClass.Application, $"Failed to convert the received {RyujinxApp.FullAppName} version from GitHub!"); - - await ContentDialogHelper.CreateWarningDialog( - LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage], - LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]); - - _running = false; - - return default; - } - - return (currentVersion, newVersion); - } + private static string _stableUrlFormat = null; + private static string _changelogUrlFormat = null; public static async Task BeginUpdateAsync(bool showVersionUpToDate = false) { @@ -179,7 +65,18 @@ namespace Ryujinx.Ava.Systems _running = true; - Optional<(Version, Version)> versionTuple = await CheckVersionAsync(showVersionUpToDate); + Optional<(Version, Version)> versionTuple; + + try + { + versionTuple = await CheckGitLabVersionAsync(showVersionUpToDate); + } + catch (Exception e) + { + Logger.Error?.PrintMsg(LogClass.Application, "Update checking from GitLab failed; falling back to GitHub."); + Logger.Error?.PrintMsg(LogClass.Application, e.Message); + versionTuple = await CheckGitHubVersionAsync(showVersionUpToDate); + } if (_running is false || !versionTuple.HasValue) return; @@ -196,7 +93,7 @@ namespace Ryujinx.Ava.Systems if (userResult is UserResult.Ok) { - OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion, _currentReleaseChannel.Value)); + OpenHelper.OpenUrl(_changelogUrlFormat.Format(currentVersion)); } } @@ -247,7 +144,7 @@ namespace Ryujinx.Ava.Systems break; // Secondary button maps to no, which in this case is the show changelog button. case UserResult.No: - OpenHelper.OpenUrl(ReleaseInformation.GetChangelogUrl(currentVersion, newVersion, _currentReleaseChannel.Value)); + OpenHelper.OpenUrl(ReleaseInformation.GetChangelogUrl(currentVersion, newVersion, _changelogUrlFormat.Format(currentVersion))); goto RequestUserToUpdate; default: _running = false; diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index a2d7ff657..d438342a4 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -419,7 +419,7 @@ namespace Ryujinx.Ava.UI.Windows .Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}")); break; case UpdaterType.CheckInBackground: - if ((await Updater.CheckVersionAsync()).TryGet(out (Version Current, Version Incoming) versions)) + if ((await Updater.CheckGitHubVersionAsync()).TryGet(out (Version Current, Version Incoming) versions)) { Dispatcher.UIThread.Post(() => RyujinxApp.MainWindow.ViewModel.UpdateAvailable = versions.Current < versions.Incoming); }