//
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License 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
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Documents;
using MaaWpfGui.Constants;
using MaaWpfGui.Helper;
using MaaWpfGui.Services;
using MaaWpfGui.Utilities;
using Markdig;
using Neo.Markdig.Xaml;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Stylet;
using StyletIoC;
namespace MaaWpfGui.ViewModels.UI
{
///
/// The view model of version update.
///
public class VersionUpdateViewModel : Screen
{
private readonly SettingsViewModel _settingsViewModel;
private readonly TaskQueueViewModel _taskQueueViewModel;
private readonly IHttpService _httpService;
///
/// Initializes a new instance of the class.
///
/// The IoC container.
public VersionUpdateViewModel(IContainer container)
{
_settingsViewModel = container.Get();
_taskQueueViewModel = container.Get();
_httpService = container.Get();
}
[DllImport("MaaCore.dll")]
private static extern IntPtr AsstGetVersion();
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(AsstGetVersion());
private string _latestVersion;
private string _updateTag = ConfigurationHelper.GetValue(ConfigurationKeys.VersionName, string.Empty);
///
/// Gets or sets the update tag.
///
public string UpdateTag
{
get => _updateTag;
set
{
SetAndNotify(ref _updateTag, value);
ConfigurationHelper.SetValue(ConfigurationKeys.VersionName, value);
}
}
private string _updateInfo = ConfigurationHelper.GetValue(ConfigurationKeys.VersionUpdateBody, string.Empty);
// private static readonly MarkdownPipeline s_markdownPipeline = new MarkdownPipelineBuilder().UseXamlSupportedExtensions().Build();
///
/// Gets or sets the update info.
///
public string UpdateInfo
{
get
{
try
{
return AddContributorLink(_updateInfo);
}
catch
{
return _updateInfo;
}
}
set
{
SetAndNotify(ref _updateInfo, value);
ConfigurationHelper.SetValue(ConfigurationKeys.VersionUpdateBody, value);
}
}
public FlowDocument UpdateInfoDoc => MarkdownXaml.ToFlowDocument(UpdateInfo,
new MarkdownPipelineBuilder().UseXamlSupportedExtensions().Build());
private string _updateUrl;
///
/// Gets or sets the update URL.
///
public string UpdateUrl
{
get => _updateUrl;
set => SetAndNotify(ref _updateUrl, value);
}
private bool _isFirstBootAfterUpdate = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.VersionUpdateIsFirstBoot, bool.FalseString));
///
/// Gets or sets a value indicating whether it is the first boot after updating.
///
public bool IsFirstBootAfterUpdate
{
get => _isFirstBootAfterUpdate;
set
{
SetAndNotify(ref _isFirstBootAfterUpdate, value);
ConfigurationHelper.SetValue(ConfigurationKeys.VersionUpdateIsFirstBoot, value.ToString());
}
}
private string _updatePackageName = ConfigurationHelper.GetValue(ConfigurationKeys.VersionUpdatePackage, string.Empty);
///
/// Gets or sets the name of the update package.
///
public string UpdatePackageName
{
get => _updatePackageName;
set
{
SetAndNotify(ref _updatePackageName, value);
ConfigurationHelper.SetValue(ConfigurationKeys.VersionUpdatePackage, value);
}
}
///
/// Gets the OS architecture.
///
public static string OSArchitecture => RuntimeInformation.OSArchitecture.ToString().ToLower();
///
/// Gets a value indicating whether the OS is arm.
///
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 JObject _latestJson;
private JObject _assetsObject;
///
/// 检查是否有已下载的更新包,如果有立即更新并重启进程。
///
/// 操作成功返回 ,反之则返回 。
public bool CheckAndUpdateNow()
{
if (UpdateTag == string.Empty
|| UpdatePackageName == string.Empty
|| !File.Exists(UpdatePackageName))
{
return false;
}
Execute.OnUIThread(() =>
{
using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionZipFileFoundTitle"));
toast.AppendContentText(LocalizationHelper.GetString("NewVersionZipFileFoundDescDecompressing"))
.AppendContentText(UpdateTag)
.ShowUpdateVersion(row: 2);
});
string curDir = Directory.GetCurrentDirectory();
string extractDir = Path.Combine(curDir, "NewVersionExtract");
string oldFileDir = Path.Combine(curDir, ".old");
// 解压
try
{
if (Directory.Exists(extractDir))
{
Directory.Delete(extractDir, true);
}
System.IO.Compression.ZipFile.ExtractToDirectory(UpdatePackageName, extractDir);
}
catch (InvalidDataException)
{
File.Delete(UpdatePackageName);
Execute.OnUIThread(() =>
{
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");
if (File.Exists(removeListFile))
{
string[] removeList = File.ReadAllLines(removeListFile);
foreach (string file in removeList)
{
string path = Path.Combine(curDir, file);
if (File.Exists(path))
{
string moveTo = Path.Combine(oldFileDir, file);
if (File.Exists(moveTo))
{
File.Delete(moveTo);
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(moveTo));
}
File.Move(path, moveTo);
}
}
}
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);
if (fileName == "removelist.txt" || fileName == "md5sum.txt")
{
continue;
}
string curFileName = file.Replace(extractDir, curDir);
if (File.Exists(curFileName))
{
string moveTo = file.Replace(extractDir, oldFileDir);
if (File.Exists(moveTo))
{
File.Delete(moveTo);
}
File.Move(curFileName, moveTo);
}
File.Move(file, curFileName);
}
foreach (var oldFile in Directory.GetFiles(curDir, "*.old"))
{
File.Delete(oldFile);
}
// 操作完了,把解压的文件删了
Directory.Delete(extractDir, true);
File.Delete(UpdatePackageName);
// 保存更新信息,下次启动后会弹出已更新完成的提示
UpdatePackageName = string.Empty;
IsFirstBootAfterUpdate = true;
ConfigurationHelper.Release();
// 重启进程(启动的是更新后的程序了)
var newProcess = new Process();
newProcess.StartInfo.FileName = AppDomain.CurrentDomain.FriendlyName;
newProcess.StartInfo.WorkingDirectory = Directory.GetCurrentDirectory();
newProcess.Start();
Application.Current.Shutdown();
return true;
}
public enum CheckUpdateRetT
{
///
/// 操作成功
///
OK,
///
/// 未知错误
///
UnknownError,
///
/// 无需更新
///
NoNeedToUpdate,
///
/// 已经是最新版
///
AlreadyLatest,
///
/// 网络错误
///
NetworkError,
///
/// 获取信息失败
///
FailedToGetInfo,
///
/// 新版正在构建中
///
NewVersionIsBeingBuilt,
}
public enum Downloader
{
///
/// 原生下载器
///
Native,
}
///
/// 检查更新,并下载更新包。
///
/// 是否强制检查。
/// 操作成功返回 ,反之则返回 。
public CheckUpdateRetT CheckAndDownloadUpdate(bool force = false)
{
_settingsViewModel.IsCheckingForUpdates = true;
var checkResult = ((Func)(() =>
{
// 检查更新
var checkRet = CheckUpdate(force);
if (checkRet != CheckUpdateRetT.OK)
{
return checkRet;
}
// 保存新版本的信息
var name = _latestJson["name"]?.ToString();
UpdateTag = name == string.Empty ? _latestJson["tag_name"]?.ToString() : name;
var body = _latestJson["body"]?.ToString();
if (body == string.Empty)
{
string ComparableHash(string version)
{
if (isStdVersion(version))
{
return version;
}
else if (Semver.SemVersion.TryParse(version, Semver.SemVersionStyles.AllowLowerV, out var semVersion) &&
isNightlyVersion(semVersion))
{
// v4.6.6-1.g{Hash}
// v4.6.7-beta.2.8.g{Hash}
var commitHash = semVersion.PrereleaseIdentifiers.Last().ToString();
if (commitHash.StartsWith("g"))
{
commitHash = commitHash.Remove(0, 1);
}
return commitHash;
}
return null;
}
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();
bool otaFound = _assetsObject != null;
bool goDownload = otaFound && _settingsViewModel.AutoDownloadUpdatePackage;
#pragma warning disable IDE0042
var openUrlToastButton = (
text: LocalizationHelper.GetString("NewVersionFoundButtonGoWebpage"),
action: new Action(() =>
{
if (!string.IsNullOrWhiteSpace(UpdateUrl))
{
Process.Start(UpdateUrl);
}
}));
#pragma warning restore IDE0042
Execute.OnUIThread(() =>
{
using var toast = new ToastNotification(otaFound ?
LocalizationHelper.GetString("NewVersionFoundTitle") :
LocalizationHelper.GetString("NewVersionFoundButNoPackageTitle"));
if (goDownload)
{
OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadPreparing"));
toast.AppendContentText(LocalizationHelper.GetString("NewVersionFoundDescDownloading"));
}
toast.AppendContentText(LocalizationHelper.GetString("NewVersionFoundDescId") + UpdateTag);
if (!otaFound)
{
toast.AppendContentText(LocalizationHelper.GetString("NewVersionFoundButNoPackageDesc"));
}
var toastDesc = UpdateInfo.Length > 100 ?
UpdateInfo.Substring(0, 100) + "..." :
UpdateInfo;
toast.AppendContentText(LocalizationHelper.GetString("NewVersionFoundDescInfo") + toastDesc);
toast.AddButtonLeft(openUrlToastButton.text, openUrlToastButton.action);
toast.ButtonSystemUrl = UpdateUrl;
toast.ShowUpdateVersion();
});
UpdatePackageName = _assetsObject?["name"]?.ToString() ?? string.Empty;
if (!goDownload || string.IsNullOrWhiteSpace(UpdatePackageName))
{
OutputDownloadProgress(string.Empty);
return CheckUpdateRetT.NoNeedToUpdate;
}
// 下载压缩包
var downloaded = false;
var mirroredReplaceMap = new List>
{
new Tuple("github.com", "agent.imgg.dev"),
new Tuple("https://", "https://git.114514.pro/https://"),
new Tuple("https://", "https://ghproxy.com/https://"),
new Tuple("github.com", "ota.maa.plus"),
new Tuple("github.com", "download.fastgit.org"),
null,
};
string rawUrl = _assetsObject["browser_download_url"]?.ToString();
const int DownloadRetryMaxTimes = 1;
for (int i = 0; i <= DownloadRetryMaxTimes && !downloaded; i++)
{
var url = rawUrl;
foreach (var repTuple in mirroredReplaceMap)
{
if (repTuple != null)
{
url = url.Replace(repTuple.Item1, repTuple.Item2);
}
if (DownloadGithubAssets(url, _assetsObject))
{
OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadCompletedTitle"));
downloaded = true;
break;
}
}
}
if (!downloaded)
{
OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
Execute.OnUIThread(() =>
{
using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
toast.ButtonSystemUrl = UpdateUrl;
toast.AppendContentText(LocalizationHelper.GetString("NewVersionDownloadFailedDesc"))
.AddButtonLeft(openUrlToastButton.text, openUrlToastButton.action)
.Show();
});
return CheckUpdateRetT.NoNeedToUpdate;
}
return CheckUpdateRetT.OK;
}))();
_settingsViewModel.IsCheckingForUpdates = false;
return checkResult;
}
public void AskToRestart()
{
MessageBoxHelper.Unregister();
MessageBoxHelper.Yes = LocalizationHelper.GetString("Ok");
MessageBoxHelper.No = LocalizationHelper.GetString("ManualRestart");
MessageBoxHelper.Register();
var result = MessageBox.Show(
LocalizationHelper.GetString("NewVersionDownloadCompletedDesc"),
LocalizationHelper.GetString("NewVersionDownloadCompletedTitle"),
MessageBoxButton.YesNo,
MessageBoxImage.Question);
MessageBoxHelper.Unregister();
if (result == MessageBoxResult.Yes)
{
Application.Current.Shutdown();
System.Windows.Forms.Application.Restart();
}
}
///
/// 检查更新。
///
/// 是否强制检查。
/// 检查到更新返回 ,反之则返回 。
private CheckUpdateRetT CheckUpdate(bool force = false)
{
// 自动更新或者手动触发
if (!(_settingsViewModel.UpdateCheck || force))
{
return CheckUpdateRetT.NoNeedToUpdate;
}
// 调试版不检查更新
if (isDebugVersion())
{
return CheckUpdateRetT.FailedToGetInfo;
}
const int RequestRetryMaxTimes = 2;
try
{
if (!_settingsViewModel.UpdateBeta && !_settingsViewModel.UpdateNightly)
{
// 稳定版更新使用主仓库 /latest 接口
// 直接使用 MaaRelease 的话,30 个可能会找不到稳定版,因为有可能 Nightly 发了很多
var stableResponse = RequestGithubApi(StableRequestUrl, RequestRetryMaxTimes);
if (string.IsNullOrEmpty(stableResponse))
{
return CheckUpdateRetT.NetworkError;
}
_latestJson = JsonConvert.DeserializeObject(stableResponse) as JObject;
_latestVersion = _latestJson["tag_name"].ToString();
stableResponse = RequestGithubApi(MaaReleaseRequestUrlByTag + _latestVersion, RequestRetryMaxTimes);
// 主仓库能找到版,但是 MaaRelease 找不到,说明 MaaRelease 还没有同步(一般过个十分钟就同步好了)
if (string.IsNullOrEmpty(stableResponse))
{
return CheckUpdateRetT.NewVersionIsBeingBuilt;
}
_latestJson = JsonConvert.DeserializeObject(stableResponse) as JObject;
}
else
{
// 非稳定版更新使用 MaaRelease/releases 接口
var response = RequestGithubApi(RequestUrl, RequestRetryMaxTimes);
if (string.IsNullOrEmpty(response))
{
return CheckUpdateRetT.NetworkError;
}
var releaseArray = JsonConvert.DeserializeObject(response) as JArray;
_latestJson = null;
foreach (var item in releaseArray)
{
if (!_settingsViewModel.UpdateNightly && !isStdVersion(item["tag_name"].ToString()))
{
continue;
}
_latestJson = item as JObject;
break;
}
}
if (_latestJson == null)
{
return CheckUpdateRetT.AlreadyLatest;
}
_latestVersion = _latestJson["tag_name"].ToString();
var releaseAssets = _latestJson["assets"] as JArray;
if (_settingsViewModel.UpdateNightly)
{
if (_curVersion == _latestVersion)
{
return CheckUpdateRetT.AlreadyLatest;
}
}
else
{
bool curParsed = Semver.SemVersion.TryParse(_curVersion, Semver.SemVersionStyles.AllowLowerV, out var curVersionObj);
bool latestPared = Semver.SemVersion.TryParse(_latestVersion, Semver.SemVersionStyles.AllowLowerV, out var latestVersionObj);
if (curParsed && latestPared)
{
if (curVersionObj.CompareSortOrderTo(latestVersionObj) >= 0)
{
return CheckUpdateRetT.AlreadyLatest;
}
}
else if (string.CompareOrdinal(_curVersion, _latestVersion) >= 0)
{
return CheckUpdateRetT.AlreadyLatest;
}
}
// 从主仓库获取changelog等信息
// 非稳定版本是 Nightly 下载的,主仓库没有它的更新信息,不必请求
if (isStdVersion(_latestVersion))
{
var infoResponse = RequestGithubApi(InfoRequestUrl + _latestVersion, RequestRetryMaxTimes);
if (string.IsNullOrEmpty(infoResponse))
{
return CheckUpdateRetT.FailedToGetInfo;
}
_latestJson = JsonConvert.DeserializeObject(infoResponse) as JObject;
}
_assetsObject = null;
foreach (var curAssets in releaseAssets)
{
string name = curAssets["name"].ToString().ToLower();
if (name.Contains("ota") && name.Contains("win") && name.Contains($"{_curVersion}_{_latestVersion}"))
{
_assetsObject = curAssets as JObject;
if (IsArm ^ name.Contains("arm"))
{
continue; // 兼容旧版本,以前 ota 不区分指令集架构
}
else
{
break;
}
}
}
return CheckUpdateRetT.OK;
}
catch (Exception e)
{
// Refactor pending
return CheckUpdateRetT.UnknownError;
}
}
private string RequestGithubApi(string url, int retryTimes)
{
string response = string.Empty;
string[] requestSource = { "https://api.github.com/", "https://api.kgithub.com/" };
do
{
for (var i = 0; i < requestSource.Length; i++)
{
response = _httpService.GetStringAsync(new Uri(requestSource[i] + url)).ConfigureAwait(false).GetAwaiter().GetResult();
if (!string.IsNullOrEmpty(response))
{
break;
}
}
}
while (string.IsNullOrEmpty(response) && retryTimes-- > 0);
return response;
}
///
/// 获取 GitHub Assets 对象对应的文件
///
/// 下载链接
/// Github Assets 对象
/// 操作成功返回 true,反之则返回 false
private bool DownloadGithubAssets(string url, JObject assetsObject)
{
_logItemViewModels = _taskQueueViewModel.LogItemViewModels;
return _httpService.DownloadFileAsync(
new Uri(url),
assetsObject["name"].ToString(),
assetsObject["content_type"].ToString())
.ConfigureAwait(false).GetAwaiter().GetResult();
}
private static System.Collections.ObjectModel.ObservableCollection _logItemViewModels = null;
public static void OutputDownloadProgress(long value = 0, long maximum = 1, int len = 0, double ts = 1)
{
OutputDownloadProgress(
string.Format("[{0:F}MiB/{1:F}MiB({2}%) {3:F} KiB/s]",
value / 1048576.0,
maximum / 1048576.0,
100 * value / maximum,
len / ts / 1024.0));
}
private static void OutputDownloadProgress(string output, bool downloading = true)
{
if (_logItemViewModels == null)
{
return;
}
var log = new LogItemViewModel(downloading ? LocalizationHelper.GetString("NewVersionFoundDescDownloading") + "\n" + output : output, UiLogColor.Download);
Application.Current.Dispatcher.Invoke(() =>
{
if (_logItemViewModels.Count > 0 && _logItemViewModels[0].Color == UiLogColor.Download)
{
if (!string.IsNullOrEmpty(output))
{
_logItemViewModels[0] = log;
}
else
{
_logItemViewModels.RemoveAt(0);
}
}
else if (!string.IsNullOrEmpty(output))
{
_logItemViewModels.Clear();
_logItemViewModels.Add(log);
}
});
}
private bool isDebugVersion(string version = null)
{
version ??= _curVersion;
return version == "DEBUG VERSION";
}
private 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 Compiled:c{CommitHash[..7]}
version ??= _curVersion;
if (isDebugVersion(version))
{
return false;
}
else if (version.StartsWith("c") || version.StartsWith("20") || version.Contains("Local"))
{
return false;
}
else if (!Semver.SemVersion.TryParse(version, Semver.SemVersionStyles.AllowLowerV, out var semVersion))
{
return false;
}
else if (isNightlyVersion(semVersion))
{
return false;
}
return true;
}
private bool isNightlyVersion(Semver.SemVersion version)
{
if (!version.IsPrerelease)
{
return false;
}
// 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;
}
///
/// 复制文件夹内容并覆盖已存在的相同名字的文件
///
/// 源文件夹
/// 目标文件夹
public static void CopyFilesRecursively(string sourcePath, string targetPath)
{
Directory.CreateDirectory(targetPath);
// Now Create all of the directories
foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath));
}
// Copy all the files & Replaces any files with the same name
foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
{
File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
}
}
///
/// The event handler of opening hyperlink.
///
/// The sender.
/// The event arguments.
public void OpenHyperlink(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
Process.Start(e.Parameter.ToString());
}
}
}