Files
MaaAssistantArknights/src/MaaWpfGui/ViewModels/Dialogs/VersionUpdateDialogViewModel.cs
Status102 e8ec73a67c perf(wpf): 当热更资源命中时, 不再二次加载资源 (#15907)
perf: 当热更资源命中时, 不再二次加载资源
2026-03-08 20:49:17 +08:00

1298 lines
44 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// <copyright file="VersionUpdateDialogViewModel.cs" company="MaaAssistantArknights">
// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team)
// Copyright (C) 2021-2025 MaaAssistantArknights Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License v3.0 only as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using MaaWpfGui.Constants;
using MaaWpfGui.Constants.Enums;
using MaaWpfGui.Extensions;
using MaaWpfGui.Helper;
using MaaWpfGui.Main;
using MaaWpfGui.Models;
using MaaWpfGui.Services;
using MaaWpfGui.States;
using MaaWpfGui.Utilities;
using MaaWpfGui.ViewModels.UI;
using MaaWpfGui.ViewModels.UserControl.Settings;
using Microsoft.VisualBasic.FileIO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Semver;
using Serilog;
using Stylet;
using SearchOption = System.IO.SearchOption;
namespace MaaWpfGui.ViewModels.Dialogs;
/// <summary>
/// The view model of version update.
/// </summary>
public class VersionUpdateDialogViewModel : Screen
{
private readonly RunningState _runningState;
/// <summary>
/// Initializes a new instance of the <see cref="VersionUpdateDialogViewModel"/> class.
/// </summary>
public VersionUpdateDialogViewModel()
{
_runningState = RunningState.Instance;
}
private static readonly ILogger _logger = Log.ForContext<VersionUpdateDialogViewModel>();
private static string AddContributorLink(string text)
{
/*
// "@ " -> "@ "
// "`@`" -> "`@`"
// "@MistEO" -> "[@MistEO](https://github.com/MistEO)"
// "[@MistEO]" -> "[@MistEO]"
*/
return Regex.Replace(text, @"([^\[`]|^)@([^\s]+)", "$1[@$2](https://github.com/$2)");
}
private readonly string _curVersion = Marshal.PtrToStringAnsi(MaaService.AsstGetVersion()) ?? "0.0.1";
private string _latestVersion = string.Empty;
private string _updateTag = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionName, string.Empty);
/// <summary>
/// Gets or sets the update tag.
/// </summary>
public string UpdateTag
{
get => _updateTag;
set {
SetAndNotify(ref _updateTag, value);
ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionName, value);
}
}
private string _updateInfo = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionUpdateBody, string.Empty);
// private static readonly MarkdownPipeline s_markdownPipeline = new MarkdownPipelineBuilder().UseXamlSupportedExtensions().Build();
/// <summary>
/// Gets or sets the update info.
/// </summary>
public string UpdateInfo
{
get {
try
{
return AddContributorLink(_updateInfo);
}
catch
{
return _updateInfo;
}
}
set {
SetAndNotify(ref _updateInfo, value);
ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionUpdateBody, value);
}
}
private string _updateUrl = string.Empty;
/// <summary>
/// Gets or sets the update URL.
/// </summary>
public string UpdateUrl
{
get => _updateUrl;
set => SetAndNotify(ref _updateUrl, value);
}
private bool _isFirstBootAfterUpdate = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionUpdateIsFirstBoot, bool.FalseString));
/// <summary>
/// Gets or sets a value indicating whether it is the first boot after updating.
/// </summary>
public bool IsFirstBootAfterUpdate
{
get => _isFirstBootAfterUpdate;
set {
SetAndNotify(ref _isFirstBootAfterUpdate, value);
ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionUpdateIsFirstBoot, value.ToString());
}
}
private string _updatePackageName = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionUpdatePackage, string.Empty);
/// <summary>
/// Gets or sets the name of the update package.
/// </summary>
public string UpdatePackageName
{
get => _updatePackageName;
set {
SetAndNotify(ref _updatePackageName, value);
ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionUpdatePackage, value);
}
}
/// <summary>
/// Gets the OS architecture.
/// </summary>
private static string OsArchitecture => RuntimeInformation.OSArchitecture.ToString().ToLower();
/// <summary>
/// Gets a value indicating whether the OS is arm.
/// </summary>
public static bool IsArm => OsArchitecture.StartsWith("arm");
/*
private const string RequestUrl = "repos/MaaAssistantArknights/MaaRelease/releases";
private const string StableRequestUrl = "repos/MaaAssistantArknights/MaaAssistantArknights/releases/latest";
private const string MaaReleaseRequestUrlByTag = "repos/MaaAssistantArknights/MaaRelease/releases/tags/";
private const string InfoRequestUrl = "repos/MaaAssistantArknights/MaaAssistantArknights/releases/tags/";
*/
private const string MaaUpdateApi = "version/summary.json";
private JObject? _latestJson;
private JObject? _assetsObject;
private string? _mirrorcDownloadUrl;
private string? _mirrorcVersionName;
private string? _mirrorcReleaseNote;
/// <summary>
/// 检查是否有已下载的更新包
/// </summary>
/// <returns>操作成功返回 <see langword="true"/>,反之则返回 <see langword="false"/>。</returns>
public bool CheckAndUpdateNow()
{
if (UpdateTag == string.Empty || UpdatePackageName == string.Empty || !File.Exists(UpdatePackageName))
{
return false;
}
{
using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionZipFileFoundTitle"));
toast.AppendContentText(LocalizationHelper.GetString("NewVersionZipFileFoundDescDecompressing"))
.AppendContentText(UpdateTag)
.ShowUpdateVersion(row: 2);
}
string curDir = PathsHelper.BaseDir;
string extractDir = Path.Combine(curDir, "NewVersionExtract"); // 新版本解压的路径
string oldFileDir = Path.Combine(curDir, ".old");
// 解压
try
{
if (Directory.Exists(extractDir))
{
Directory.Delete(extractDir, true);
}
ZipFile.ExtractToDirectory(UpdatePackageName, extractDir);
}
catch (InvalidDataException)
{
File.Delete(UpdatePackageName);
{
using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionZipFileBrokenTitle"));
toast.AppendContentText(LocalizationHelper.GetString("NewVersionZipFileBrokenDescFilename") + UpdatePackageName)
.AppendContentText(LocalizationHelper.GetString("NewVersionZipFileBrokenDescDeleted"))
.ShowUpdateVersion();
}
return false;
}
string removeListFile = Path.Combine(extractDir, "removelist.txt");
string mirrorChyanChangeFile = Path.Combine(extractDir, "changes.json");
bool isOTAPackage = File.Exists(removeListFile) || File.Exists(mirrorChyanChangeFile);
string[] removeList = [];
if (File.Exists(removeListFile))
{
removeList = File.ReadAllLines(removeListFile);
}
if (File.Exists(mirrorChyanChangeFile))
{
try
{
string json = File.ReadAllText(mirrorChyanChangeFile);
var jObject = JObject.Parse(json);
removeList = jObject["deleted"]?.ToObject<string[]>() ?? [];
}
catch (Exception e)
{
_logger.Error("parse mirrorChyan changes.json error: {EMessage}", e.Message);
}
}
if (removeList.Length > 0)
{
foreach (string file in removeList)
{
string path = Path.Combine(curDir, file);
if (!File.Exists(path))
{
continue;
}
string moveTo = Path.Combine(oldFileDir, file);
if (File.Exists(moveTo))
{
DeleteFileWithBackup(moveTo);
}
else
{
var dir = Path.GetDirectoryName(moveTo);
if (dir != null)
{
Directory.CreateDirectory(dir);
}
}
try
{
File.Move(path, moveTo);
}
catch (Exception e)
{
_logger.Error("move file error, path: {Path}, moveTo: {MoveTo}, error: {EMessage}", path, moveTo, e.Message);
throw;
}
}
}
else if (!isOTAPackage)
{
List<Task> deleteTasks = [];
foreach (var dir in Directory.GetDirectories(extractDir))
{
deleteTasks.Add(Task.Run(() => {
try
{
FileSystem.DeleteDirectory(dir.Replace(extractDir, curDir), UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
}
catch
{
_logger.Error("delete directory error, dir: {Dir}", dir);
}
}));
}
Task.WaitAll([.. deleteTasks]);
}
Directory.CreateDirectory(oldFileDir);
foreach (var dir in Directory.GetDirectories(extractDir, "*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(dir.Replace(extractDir, curDir));
Directory.CreateDirectory(dir.Replace(extractDir, oldFileDir));
}
// 复制新版本的所有文件到当前路径下
foreach (var file in Directory.GetFiles(extractDir, "*", SearchOption.AllDirectories))
{
var fileName = Path.GetFileName(file);
// ReSharper disable once StringLiteralTypo
if (fileName == "removelist.txt")
{
continue;
}
string curFileName = file.Replace(extractDir, curDir);
try
{
if (File.Exists(curFileName))
{
string moveTo = file.Replace(extractDir, oldFileDir);
if (File.Exists(moveTo))
{
DeleteFileWithBackup(moveTo);
}
File.Move(curFileName, moveTo);
}
File.Move(file, curFileName);
}
catch (Exception e)
{
_logger.Error("move file error, file name: {File}, error: {EMessage}", file, e.Message);
throw;
}
}
// 操作完了,把解压的文件删了
Directory.Delete(extractDir, true);
File.Delete(UpdatePackageName);
// 保存更新信息,下次启动后会弹出已更新完成的提示
UpdatePackageName = string.Empty;
IsFirstBootAfterUpdate = true;
return true;
static void DeleteFileWithBackup(string filePath)
{
try
{
File.Delete(filePath);
}
catch (Exception e)
{
_logger.Error("delete file error, filePath: {FilePath}, error: {EMessage}, try to backup.", filePath, e.Message);
int index = 0;
string currentDate = DateTime.Now.ToString("yyyyMMddHHmm");
string backupFilePath = $"{filePath}.{currentDate}.{index}";
while (File.Exists(backupFilePath))
{
index++;
backupFilePath = $"{filePath}.{currentDate}.{index}";
}
try
{
File.Move(filePath, backupFilePath);
}
catch (Exception e1)
{
_logger.Error("move file error, path: {FilePath}, moveTo: {BackupFilePath}, error: {E1Message}", filePath, backupFilePath, e1.Message);
throw;
}
}
}
}
public enum CheckUpdateRetT
{
/// <summary>
/// 操作成功
/// </summary>
// ReSharper disable once InconsistentNaming
OK,
/// <summary>
/// 未知错误
/// </summary>
UnknownError,
/// <summary>
/// 无需更新
/// </summary>
NoNeedToUpdate,
/// <summary>
/// 调试版本无需更新
/// </summary>
NoNeedToUpdateDebugVersion,
/// <summary>
/// 已经是最新版
/// </summary>
AlreadyLatest,
/// <summary>
/// 网络错误
/// </summary>
NetworkError,
/// <summary>
/// 获取信息失败
/// </summary>
FailedToGetInfo,
/// <summary>
/// 新版正在构建中
/// </summary>
NewVersionIsBeingBuilt,
/// <summary>
/// 只更新了游戏资源
/// </summary>
OnlyGameResourceUpdated,
/// <summary>
/// NoMirrorChyanCdk
/// </summary>
NoMirrorChyanCdk,
}
public enum AppUpdateSource
{
/// <summary>
/// Maa API
/// </summary>
MaaApi,
/// <summary>
/// MirrorChyan
/// </summary>
MirrorChyan,
}
private bool _doNotShowUpdate = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionUpdateDoNotShowUpdate, bool.FalseString));
/// <summary>
/// Gets or sets a value indicating whether to show the update.
/// </summary>
public bool DoNotShowUpdate
{
get => _doNotShowUpdate;
set {
SetAndNotify(ref _doNotShowUpdate, value);
ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionUpdateDoNotShowUpdate, value.ToString());
}
}
/// <summary>
/// 如果是在更新后第一次启动显示ReleaseNote弹窗否则检查更新并下载更新包。
/// </summary>
/// <returns>Task</returns>
public async Task ShowUpdateOrDownload()
{
if (IsFirstBootAfterUpdate)
{
IsFirstBootAfterUpdate = false;
if (!DoNotShowUpdate)
{
Instances.WindowManager.ShowWindow(this);
}
}
else
{
if (!SettingsViewModel.VersionUpdateSettings.StartupUpdateCheck)
{
return;
}
if (!IsDebugVersion())
{
if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "MirrorChyan" && string.IsNullOrEmpty(SettingsViewModel.VersionUpdateSettings.MirrorChyanCdk))
{
_ = Task.Run(() =>
MessageBoxHelper.Show(
LocalizationHelper.GetString("MirrorChyanSelectedButNoCdk"),
"cdk is empty!",
MessageBoxButton.OK,
MessageBoxImage.Warning,
ok: LocalizationHelper.GetString("Ok")));
}
await VersionUpdateAndAskToRestartAsync();
await ResourceUpdater.ResourceUpdateAndReloadAsync();
}
else
{
// await ResourceUpdater.CheckAndDownloadResourceUpdate();
// 跑个空任务避免 async warning
await Task.Run(() => { });
}
}
}
/// <summary>
/// 检查更新并下载更新包,如果成功则提示重启。
/// </summary>
/// <returns>Task</returns>
public async Task VersionUpdateAndAskToRestartAsync()
{
if (SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates)
{
return;
}
var ret = await CheckAndDownloadVersionUpdate();
if (ret == CheckUpdateRetT.OK)
{
_ = AskToRestart();
}
var toastMessage = ret switch {
CheckUpdateRetT.NoNeedToUpdate => string.Empty,
CheckUpdateRetT.NoNeedToUpdateDebugVersion => string.Empty,
CheckUpdateRetT.AlreadyLatest => string.Empty,
CheckUpdateRetT.UnknownError => LocalizationHelper.GetString("NewVersionDetectFailedTitle"),
CheckUpdateRetT.NetworkError => LocalizationHelper.GetString("CheckNetworking"),
CheckUpdateRetT.FailedToGetInfo => LocalizationHelper.GetString("GetReleaseNoteFailed"),
CheckUpdateRetT.OK => string.Empty,
CheckUpdateRetT.NewVersionIsBeingBuilt => string.Empty,
CheckUpdateRetT.OnlyGameResourceUpdated => string.Empty,
CheckUpdateRetT.NoMirrorChyanCdk => LocalizationHelper.GetString("MirrorChyanSelectedButNoCdk"),
_ => string.Empty,
};
if (toastMessage != string.Empty)
{
ToastNotification.ShowDirect(toastMessage);
}
}
/// <summary>
/// 检查更新,并下载更新包。
/// </summary>
/// <returns>操作成功返回 <see langword="true"/>,反之则返回 <see langword="false"/>。</returns>
public async Task<CheckUpdateRetT> CheckAndDownloadVersionUpdate()
{
try
{
SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates = true;
var (checkRet, source) = await CheckUpdate();
if (checkRet != CheckUpdateRetT.OK)
{
return checkRet;
}
return source switch {
AppUpdateSource.MaaApi => await HandleUpdateFromMaaApi(),
AppUpdateSource.MirrorChyan => await HandleUpdateFromMirrorChyan(),
_ => CheckUpdateRetT.UnknownError,
};
}
finally
{
SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates = false;
}
}
private async Task<CheckUpdateRetT> HandleUpdateFromMaaApi()
{
// 保存新版本的信息
var name = _latestJson?["name"]?.ToString();
UpdateTag = string.IsNullOrEmpty(name) ? (_latestJson?["tag_name"]?.ToString() ?? string.Empty) : name;
SettingsViewModel.VersionUpdateSettings.NewVersionFoundInfo = $"{LocalizationHelper.GetString("NewVersionFoundTitle")}: {UpdateTag}";
var body = _latestJson?["body"]?.ToString() ?? string.Empty;
if (string.IsNullOrEmpty(body))
{
var curHash = ComparableHash(_curVersion);
var latestHash = ComparableHash(_latestVersion);
if (curHash != null && latestHash != null)
{
body = $"**Full Changelog**: [{curHash} -> {latestHash}](https://github.com/MaaAssistantArknights/MaaAssistantArknights/compare/{curHash}...{latestHash})";
}
}
UpdateInfo = body;
UpdateUrl = _latestJson?["html_url"]?.ToString() ?? string.Empty;
bool otaFound = _assetsObject != null;
bool goDownload = otaFound && SettingsViewModel.VersionUpdateSettings.AutoDownloadUpdatePackage;
ShowUpdateInfo(otaFound, LocalizationHelper.GetString("NewVersionFoundButtonGoWebpage"), true);
UpdatePackageName = _assetsObject?["name"]?.ToString() ?? string.Empty;
if (!goDownload || string.IsNullOrWhiteSpace(UpdatePackageName))
{
OutputDownloadProgress(string.Empty, downloading: false);
return CheckUpdateRetT.NoNeedToUpdate;
}
if (_assetsObject == null)
{
return CheckUpdateRetT.FailedToGetInfo;
}
string? rawUrl = _assetsObject["browser_download_url"]?.ToString();
var urls = new List<string>();
if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "Github" && !SettingsViewModel.VersionUpdateSettings.ForceGithubGlobalSource)
{
var mirrors = _assetsObject["mirrors"]?.ToObject<List<string>>();
if (mirrors != null)
{
urls.AddRange(mirrors);
}
}
// 负载均衡
// var rand = new Random();
// urls = urls.OrderBy(_ => rand.Next()).ToList();
if (rawUrl != null)
{
urls.Add(rawUrl);
}
_logger.Information("Start test legacy download urls");
// run latency test parallel
var tasks = urls.ConvertAll(url => Instances.HttpService.HeadAsync(new Uri(url)));
var latencies = await Task.WhenAll(tasks);
var proxy = ConfigurationHelper.GetValue(ConfigurationKeys.UpdateProxy, string.Empty);
var hasProxy = !string.IsNullOrEmpty(proxy);
// select the fastest mirror
_logger.Information("Selecting the fastest mirror:");
var selected = 0;
for (int i = 0; i < latencies.Length; i++)
{
// ReSharper disable once StringLiteralTypo
var isInChina = urls[i].Contains("s3.maa-org.net") || urls[i].Contains("maa-ota.annangela.cn");
if (latencies[i] < 0)
{
_logger.Warning("\turl: {CDNUrl} not available", urls[i]);
continue;
}
_logger.Information("\turl: {CDNUrl}, legacy: {1:0.00}ms", urls[i], latencies[i]);
if (hasProxy && isInChina)
{
// 如果设置了代理,国内镜像的延迟加上一个固定值
latencies[i] += 6480;
}
if (latencies[selected] < 0 || (latencies[i] >= 0 && latencies[i] < latencies[selected]))
{
selected = i;
}
}
if (latencies[selected] < 0)
{
_logger.Error("All mirrors are not available");
OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
return CheckUpdateRetT.NetworkError;
}
_logger.Information("Selected mirror: {CDNUrl}", urls[selected]);
var downloaded = await DownloadGithubAssets(urls[selected], _assetsObject);
if (downloaded)
{
OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadCompletedTitle"));
}
else
{
OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
{
using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
toast.AppendContentText(LocalizationHelper.GetString("NewVersionDownloadFailedDesc"))
.AddButton(LocalizationHelper.GetString("NewVersionFoundButtonGoWebpage"), ToastNotification.GetActionTagForOpenWeb(UpdateUrl))
.Show();
}
return CheckUpdateRetT.NoNeedToUpdate;
}
return CheckUpdateRetT.OK;
string? ComparableHash(string version)
{
if (IsStdVersion(version) || IsBetaVersion(version))
{
return version;
}
if (!SemVersion.TryParse(version, SemVersionStyles.AllowLowerV, out var semVersion) ||
!IsNightlyVersion(semVersion))
{
return null;
}
// v4.6.6-1.g{Hash}
// v4.6.7-beta.2.8.g{Hash}
var commitHash = semVersion.PrereleaseIdentifiers[^1].ToString();
if (commitHash.StartsWith('g'))
{
commitHash = commitHash.Remove(0, 1);
}
return commitHash;
}
}
private void ShowUpdateInfo(bool otaFound, string? text, bool globalSource)
{
bool goDownload = otaFound && SettingsViewModel.VersionUpdateSettings.AutoDownloadUpdatePackage;
using var toast = new ToastNotification((otaFound ? LocalizationHelper.GetString("NewVersionFoundTitle") : LocalizationHelper.GetString("NewVersionFoundButNoPackageTitle")) + " : " + UpdateTag);
if (goDownload)
{
OutputDownloadProgress(LocalizationHelper.GetString("NewVersionDownloadPreparing"), false, globalSource);
toast.AppendContentText(globalSource
? LocalizationHelper.GetString("NewVersionFoundDescDownloadingWithGlobalSource")
: LocalizationHelper.GetString("NewVersionFoundDescDownloadingWithMirrorChyan"));
}
if (!otaFound)
{
toast.AppendContentText(LocalizationHelper.GetString("NewVersionFoundButNoPackageDesc"));
}
int count = 0;
foreach (var line in UpdateInfo.Split('\n'))
{
if (line.StartsWith('#') || string.IsNullOrWhiteSpace(line))
{
continue;
}
toast.AppendContentText(line);
if (++count >= 10)
{
break;
}
}
if (!string.IsNullOrEmpty(text))
{
toast.AddButton(text, ToastNotification.GetActionTagForOpenWeb(globalSource ? UpdateUrl : MaaUrls.MirrorChyanManualUpdate));
}
toast.ShowUpdateVersion();
}
private async Task<CheckUpdateRetT> HandleUpdateFromMirrorChyan()
{
if (string.IsNullOrEmpty(_mirrorcDownloadUrl))
{
return CheckUpdateRetT.FailedToGetInfo;
}
UpdateTag = _mirrorcVersionName ?? string.Empty;
UpdateInfo = _mirrorcReleaseNote ?? string.Empty;
SettingsViewModel.VersionUpdateSettings.NewVersionFoundInfo = $"{LocalizationHelper.GetString("NewVersionFoundTitle")}: {UpdateTag}";
bool goDownload = SettingsViewModel.VersionUpdateSettings.AutoDownloadUpdatePackage;
ShowUpdateInfo(true, LocalizationHelper.GetString("NewVersionFoundButtonGoWebpage"), false);
if (!goDownload)
{
OutputDownloadProgress(string.Empty, downloading: false);
return CheckUpdateRetT.NoNeedToUpdate;
}
UpdatePackageName = "MirrorChyanApp" + _mirrorcVersionName + ".zip";
var downloaded = await DownloadFromMirrorChyan(_mirrorcDownloadUrl,
UpdatePackageName);
if (downloaded)
{
OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadCompletedTitle"));
}
else
{
OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
{
using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
toast.AppendContentText(LocalizationHelper.GetString("NewVersionDownloadFailedDesc"))
.Show();
}
return CheckUpdateRetT.NoNeedToUpdate;
}
AchievementTrackerHelper.Instance.Unlock(AchievementIds.MirrorChyanFirstUse);
return CheckUpdateRetT.OK;
}
public async Task AskToRestart()
{
if (SettingsViewModel.VersionUpdateSettings.AutoInstallUpdatePackage)
{
await Bootstrapper.RestartAfterIdleAsync();
return;
}
await _runningState.UntilIdleAsync(10000);
var result = MessageBoxHelper.Show(
LocalizationHelper.GetString("NewVersionDownloadCompletedDesc"),
LocalizationHelper.GetString("NewVersionDownloadCompletedTitle"),
MessageBoxButton.OKCancel,
MessageBoxImage.Question,
ok: LocalizationHelper.GetString("Ok"),
cancel: LocalizationHelper.GetString("ManualRestart"));
if (result == MessageBoxResult.OK)
{
Bootstrapper.ShutdownAndRestartWithoutArgs();
}
}
/// <summary>
/// 检查更新。
/// </summary>
/// <returns>检查到更新返回 <see langword="true"/>,反之则返回 <see langword="false"/>。</returns>
private async Task<(CheckUpdateRetT Ret, AppUpdateSource? Source)> CheckUpdate()
{
// 调试版不检查更新
if (IsDebugVersion())
{
return (CheckUpdateRetT.NoNeedToUpdateDebugVersion, null);
}
if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "MirrorChyan")
{
try
{
var ret = await CheckUpdateByMirrorChyan();
if (ret is CheckUpdateRetT.OK or CheckUpdateRetT.AlreadyLatest)
{
return (ret, AppUpdateSource.MirrorChyan);
}
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to check update by MirrorChyan, rollback to maaApi");
}
}
try
{
var ret = await CheckUpdateByMaaApi();
return (ret, AppUpdateSource.MaaApi);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to check update by Maa API.");
return (CheckUpdateRetT.FailedToGetInfo, AppUpdateSource.MaaApi);
}
}
private async Task<CheckUpdateRetT> CheckUpdateByMaaApi()
{
var (_, json) = await Instances.MaaApiService.RequestMaaApiWithCache(MaaUpdateApi);
if (json is null)
{
_logger.Error("Failed to get update info from Maa API.");
return CheckUpdateRetT.FailedToGetInfo;
}
string versionType = SettingsViewModel.VersionUpdateSettings.VersionType switch {
VersionUpdateSettingsUserControlModel.UpdateVersionType.Beta => "beta",
VersionUpdateSettingsUserControlModel.UpdateVersionType.Nightly => "alpha",
_ => "stable",
};
var latestVersion = json[versionType]?["version"]?.ToString();
latestVersion ??= string.Empty;
if (!NeedToUpdate(latestVersion))
{
return CheckUpdateRetT.AlreadyLatest;
}
return await GetVersionDetailsByMaaApi(versionType);
}
private async Task<CheckUpdateRetT> GetVersionDetailsByMaaApi(string versionType)
{
var (_, json) = await Instances.MaaApiService.RequestMaaApiWithCache($"version/{versionType}.json", false);
if (json is null)
{
return CheckUpdateRetT.NetworkError;
}
string? latestVersion = json["version"]?.ToString();
if (string.IsNullOrEmpty(latestVersion))
{
return CheckUpdateRetT.FailedToGetInfo;
}
if (!NeedToUpdate(latestVersion))
{
return CheckUpdateRetT.AlreadyLatest;
}
_latestVersion = latestVersion;
_latestJson = json["details"] as JObject;
if (_latestJson == null)
{
return CheckUpdateRetT.FailedToGetInfo;
}
_assetsObject = null;
JObject? fullPackage = null;
var curVersionLower = _curVersion.ToLower();
var latestVersionLower = _latestVersion.ToLower();
foreach (var curAssets in ((JArray?)_latestJson["assets"])!)
{
string? name = curAssets["name"]?.ToString().ToLower();
if (name == null)
{
continue;
}
if (IsArm ^ name.Contains("arm"))
{
continue;
}
if (!name.Contains("win"))
{
continue;
}
if (name.Contains($"maa-{latestVersionLower}-"))
{
fullPackage = curAssets as JObject;
}
// ReSharper disable once InvertIf
if (name.Contains("ota") && name.Contains($"{curVersionLower}_{latestVersionLower}"))
{
_assetsObject = curAssets as JObject;
break;
}
}
if (_assetsObject == null && fullPackage != null && SettingsViewModel.VersionUpdateSettings.AutoDownloadUpdatePackage)
{
_assetsObject = fullPackage;
_logger.Warning("No OTA package found, but full package found.");
using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionNoOtaPackage"));
toast.Show(30);
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("NewVersionNoOtaPackage"), UiLogColor.Warning);
}
return CheckUpdateRetT.OK;
}
private async Task<CheckUpdateRetT> CheckUpdateByMirrorChyan()
{
var cdk = SettingsViewModel.VersionUpdateSettings.MirrorChyanCdk.Trim();
var arch = IsArm ? "arm64" : "x64";
string channel = SettingsViewModel.VersionUpdateSettings.VersionType switch {
VersionUpdateSettingsUserControlModel.UpdateVersionType.Beta => "beta",
VersionUpdateSettingsUserControlModel.UpdateVersionType.Nightly => "alpha",
_ => "stable",
};
var spid = HardwareInfoUtility.GetMachineGuid().StableHash();
var url = $"{MaaUrls.MirrorChyanAppUpdate}?current_version={_curVersion}&cdk={cdk}&user_agent=MaaWpfGui&os=win&arch={arch}&channel={channel}&sp_id={spid}";
HttpResponseMessage? response = null;
try
{
response = await Instances.HttpService.GetAsync(new(url), uriPartial: UriPartial.Path);
}
catch (Exception e)
{
_logger.Error(e, "Failed to send GET request to {Uri}", new Uri(url).GetLeftPart(UriPartial.Path));
_logger.Information("current_version: {CurVersion}, cdk: {Mask}, arch: {Arch}, channel: {Channel}", _curVersion, cdk.Mask(), arch, channel);
}
if (response is null)
{
_logger.Error("mirrorc failed");
SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true;
return CheckUpdateRetT.NetworkError;
}
var jsonStr = await response.Content.ReadAsStringAsync();
_logger.Information("{JsonStr}", jsonStr);
JObject? data = null;
try
{
data = (JObject?)JsonConvert.DeserializeObject(jsonStr);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to deserialize json");
}
if (data is null)
{
SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true;
return CheckUpdateRetT.UnknownError;
}
var mirrorChyanCdkExpired = data["data"]?["cdk_expired_time"]?.ToObject<long?>();
if (mirrorChyanCdkExpired.HasValue)
{
SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = mirrorChyanCdkExpired.Value;
SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = false;
}
else
{
SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true;
}
var errorCode = data["code"]?.ToObject<MirrorChyanErrorCode>() ?? MirrorChyanErrorCode.Undivided;
if (errorCode != MirrorChyanErrorCode.Success)
{
switch (errorCode)
{
case MirrorChyanErrorCode.KeyExpired:
ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkExpired"));
SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = false;
// 有人会第一次就填过期的 cdk 吗
if (SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime == 0)
{
SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = 1;
}
// 如果上次查出来的时间比现在的还新,说明换了 cdk重置过期时间
if (!SettingsViewModel.VersionUpdateSettings.IsMirrorChyanCdkExpired)
{
SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = mirrorChyanCdkExpired ?? 1;
}
break;
case MirrorChyanErrorCode.KeyInvalid:
ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkInvalid"));
AchievementTrackerHelper.Instance.Unlock(AchievementIds.MirrorChyanCdkError);
break;
case MirrorChyanErrorCode.ResourceQuotaExhausted:
ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkQuotaExhausted"));
break;
case MirrorChyanErrorCode.KeyMismatched:
ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkMismatched"));
break;
case MirrorChyanErrorCode.KeyBlocked:
ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkBlocked"));
break;
case MirrorChyanErrorCode.InvalidParams:
case MirrorChyanErrorCode.ResourceNotFound:
case MirrorChyanErrorCode.InvalidOs:
case MirrorChyanErrorCode.InvalidArch:
case MirrorChyanErrorCode.InvalidChannel:
case MirrorChyanErrorCode.Undivided:
ToastNotification.ShowDirect(data["msg"]?.ToString() ?? LocalizationHelper.GetString("GameResourceFailed"));
break;
}
return CheckUpdateRetT.UnknownError;
}
var version = data["data"]?["version_name"]?.ToString();
if (string.IsNullOrEmpty(version))
{
return CheckUpdateRetT.UnknownError;
}
if (!NeedToUpdate(version))
{
return CheckUpdateRetT.AlreadyLatest;
}
if (data["data"]?["update_type"]?.ToObject<string>() == "full" && SettingsViewModel.VersionUpdateSettings.AutoDownloadUpdatePackage)
{
using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionNoOtaPackage"));
toast.Show(30);
_logger.Warning("No OTA package found, but full package found.");
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("NewVersionNoOtaPackage"), UiLogColor.Warning);
}
// 到这里已经确定有新版本了
_logger.Information("New version found: {Version}", version);
_mirrorcVersionName = version;
_mirrorcReleaseNote = data["data"]?["release_note"]?.ToString();
if (string.IsNullOrEmpty(cdk))
{
return CheckUpdateRetT.NoMirrorChyanCdk;
}
_mirrorcDownloadUrl = data["data"]?["url"]?.ToString();
return CheckUpdateRetT.OK;
}
private bool NeedToUpdate(string latestVersion)
{
if (IsDebugVersion())
{
return false;
}
bool curParsed = SemVersion.TryParse(_curVersion, SemVersionStyles.AllowLowerV, out var curVersionObj);
bool latestPared = SemVersion.TryParse(latestVersion, SemVersionStyles.AllowLowerV, out var latestVersionObj);
if (curParsed && latestPared && curVersionObj != null && latestVersionObj != null)
{
return curVersionObj.CompareSortOrderTo(latestVersionObj) < 0;
}
return string.CompareOrdinal(_curVersion, latestVersion) < 0;
}
/// <summary>
/// 获取 GitHub Assets 对象对应的文件
/// </summary>
/// <param name="url">下载链接</param>
/// <param name="assetsObject">Github Assets 对象</param>
/// <returns>操作成功返回 true反之则返回 false</returns>
private static async Task<bool> DownloadGithubAssets(string url, JObject assetsObject)
{
try
{
return await Instances.HttpService.DownloadFileAsync(
new(url),
assetsObject["name"]!.ToString(),
assetsObject["content_type"]?.ToString())
.ConfigureAwait(false);
}
catch (Exception)
{
return false;
}
}
private static async Task<bool> DownloadFromMirrorChyan(string url, string filename)
{
try
{
return await Instances.HttpService.DownloadFileAsync(
new(url), filename)
.ConfigureAwait(false);
}
catch (Exception)
{
return false;
}
}
public static void OutputDownloadProgress(long value = 0, long maximum = 1, int len = 0, double ts = 1, string? toolTip = null)
{
string progress = $"[{value / 1048576.0:F}MiB/{maximum / 1048576.0:F}MiB ({value * 100.0 / maximum:F}%)";
double speedInKiBPerSecond = len / ts / 1024.0;
var speedDisplay = speedInKiBPerSecond >= 1024
? $"{speedInKiBPerSecond / 1024.0:F} MiB/s"
: $"{speedInKiBPerSecond:F} KiB/s";
OutputDownloadProgress(progress + $" {speedDisplay}", toolTip: toolTip);
}
private static bool _globalSource = true;
public static void OutputDownloadProgress(string output, bool downloading = true, bool? globalSource = null, string? toolTip = null)
{
globalSource ??= _globalSource;
_globalSource = globalSource.Value;
string fullText;
if (downloading)
{
string key = globalSource.Value ? "NewVersionFoundDescDownloadingWithGlobalSource" : "NewVersionFoundDescDownloadingWithMirrorChyan";
fullText = LocalizationHelper.GetString(key) + "\n" + output;
}
else
{
fullText = output;
}
Instances.TaskQueueViewModel?.UpdateDownloadLog(fullText, toolTip);
}
public bool IsDebugVersion(string? version = null)
{
// return false;
version ??= _curVersion;
// match case 1: DEBUG_VERSION
// match case 2: v{Major}.{Minor}.{Patch}-{CommitDistance}-g{CommitHash}
// match case 3: {CommitHash}
return Regex.IsMatch(version, @"^(.*DEBUG.*|v\d+(\.\d+){1,3}-\d+-g[0-9a-f]{6,}|[^v][0-9a-f]{6,})$");
}
public bool IsStdVersion(string? version = null)
{
// 正式版vX.X.X
// DevBuild (CI)yyyy-MM-dd-HH-mm-ss-{CommitHash[..7]}
// DevBuild (Local)yyyy-MM-dd-HH-mm-ss-{CommitHash[..7]}-Local
// Release (Local Commit)v.{CommitHash[..7]}-Local
// Release (Local Tag){Tag}-Local
// Debug (Local)DEBUG_VERSION
// Script Compiledc{CommitHash[..7]}
version ??= _curVersion;
if (IsDebugVersion(version))
{
return false;
}
if (version.StartsWith('c') || version.StartsWith("20") || version.Contains("Local"))
{
return false;
}
if (!SemVersion.TryParse(version, SemVersionStyles.AllowLowerV, out var semVersion))
{
return false;
}
return !semVersion.IsPrerelease;
}
public bool IsBetaVersion(string? version = null)
{
version ??= _curVersion;
if (IsDebugVersion(version))
{
return false;
}
if (version.StartsWith('c') || version.StartsWith("20") || version.Contains("Local"))
{
return false;
}
if (!SemVersion.TryParse(version, SemVersionStyles.AllowLowerV, out var semVersion))
{
return false;
}
return semVersion.IsPrerelease && !IsNightlyVersion(semVersion);
}
public static bool IsNightlyVersion(SemVersion version)
{
if (!version.IsPrerelease)
{
return false;
}
// ReSharper disable once CommentTypo
// v{Major}.{Minor}.{Patch}-{Prerelease}.{CommitDistance}.g{CommitHash}
// v4.6.7-beta.2.1.g1234567
// v4.6.8-5.g1234567
var lastId = version.PrereleaseIdentifiers.LastOrDefault().ToString();
return lastId.StartsWith('g') && lastId.Length >= 7;
}
}