From ab65e6ebf249aa78e0d60202531dcdcae8c00a4d Mon Sep 17 00:00:00 2001 From: dantmnf Date: Sat, 1 Apr 2023 15:59:03 +0800 Subject: [PATCH] fix: deadlock in UI thread --- src/MaaWpfGui/Services/StageManager.cs | 117 +++++++++++------- src/MaaWpfGui/Services/Web/HttpService.cs | 4 +- .../ViewModels/UI/SettingsViewModel.cs | 10 +- .../ViewModels/UI/TaskQueueViewModel.cs | 20 +-- .../ViewModels/UI/VersionUpdateViewModel.cs | 27 ++-- 5 files changed, 105 insertions(+), 73 deletions(-) diff --git a/src/MaaWpfGui/Services/StageManager.cs b/src/MaaWpfGui/Services/StageManager.cs index b195236651..e739a52359 100644 --- a/src/MaaWpfGui/Services/StageManager.cs +++ b/src/MaaWpfGui/Services/StageManager.cs @@ -32,6 +32,7 @@ using StyletIoC; namespace MaaWpfGui.Services { + /// /// Stage manager /// @@ -40,6 +41,9 @@ namespace MaaWpfGui.Services [DllImport("MaaCore.dll")] private static extern IntPtr AsstGetVersion(); + private const string StageApi = "gui/StageActivity.json"; + private const string TasksApi = "resource/tasks.json"; + // model references private readonly TaskQueueViewModel _taskQueueViewModel; private readonly IMaaApiService _maaApiService; @@ -57,49 +61,38 @@ namespace MaaWpfGui.Services { _taskQueueViewModel = container.Get(); _maaApiService = container.Get(); - UpdateStage(false); + UpdateStageLocal(); - Execute.OnUIThread(async () => + Task.Run(async () => { - var task = Task.Run(() => - { - UpdateStage(true); - }); - await task; + await UpdateStageWeb(); if (_taskQueueViewModel != null) { - _taskQueueViewModel.UpdateDatePrompt(); - _taskQueueViewModel.UpdateStageList(true); + Execute.OnUIThread(() => + { + _taskQueueViewModel.UpdateDatePrompt(); + _taskQueueViewModel.UpdateStageList(true); + }); } }); } - public void UpdateStage(bool fromWeb) + public void UpdateStageLocal() { - if (fromWeb) + UpdateStageInternal(LoadLocalStages()); + } + + public async Task UpdateStageWeb() + { + if (await CheckWebUpdate()) { - // Check if we need to update from the web - string lastUpdateTimeFile = "lastUpdateTime.json"; - JObject localLastUpdatedJson = _maaApiService.LoadApiCache(lastUpdateTimeFile); - JObject webLastUpdatedJson = _maaApiService.RequestMaaApiWithCache(lastUpdateTimeFile).ConfigureAwait(false).GetAwaiter().GetResult(); - if (localLastUpdatedJson != null && webLastUpdatedJson != null) - { - long localTimestamp = localLastUpdatedJson["timestamp"].ToObject(); - long webTimestamp = webLastUpdatedJson["timestamp"].ToObject(); - if (webTimestamp <= localTimestamp) - { - return; - } - } + UpdateStageInternal(await LoadWebStages()); + return; } + } - var tempStage = new Dictionary - { - // 这里会被 “剩余理智” 复用,第一个必须是 string.Empty 的 - // 「当前/上次」关卡导航 - { string.Empty, new StageInfo { Display = LocalizationHelper.GetString("DefaultStage"), Value = string.Empty } }, - }; - + private string GetClientType() + { var clientType = ConfigurationHelper.GetValue(ConfigurationKeys.ClientType, string.Empty); // 官服和B服使用同样的资源 @@ -108,28 +101,66 @@ namespace MaaWpfGui.Services clientType = "Official"; } - // Download the activities - const string StageApi = "gui/StageActivity.json"; - JObject activity = fromWeb - ? _maaApiService.RequestMaaApiWithCache(StageApi).ConfigureAwait(false).GetAwaiter().GetResult() - : _maaApiService.LoadApiCache(StageApi); + return clientType; + } - // Download the tasks resources into cache so MaaCore can load them later - var tasksPath = "resource/tasks.json"; - JObject tasksJson = fromWeb - ? _maaApiService.RequestMaaApiWithCache(tasksPath).ConfigureAwait(false).GetAwaiter().GetResult() - : _maaApiService.LoadApiCache(tasksPath); + private JObject LoadLocalStages() + { + JObject activity = _maaApiService.LoadApiCache(StageApi); + JObject tasksJson = _maaApiService.LoadApiCache(TasksApi); + return activity; + } + + private async Task CheckWebUpdate() + { + // Check if we need to update from the web + string lastUpdateTimeFile = "lastUpdateTime.json"; + JObject localLastUpdatedJson = _maaApiService.LoadApiCache(lastUpdateTimeFile); + JObject webLastUpdatedJson = await _maaApiService.RequestMaaApiWithCache(lastUpdateTimeFile).ConfigureAwait(false); + if (localLastUpdatedJson != null && webLastUpdatedJson != null) + { + long localTimestamp = localLastUpdatedJson["timestamp"].ToObject(); + long webTimestamp = webLastUpdatedJson["timestamp"].ToObject(); + if (webTimestamp <= localTimestamp) + { + return false; + } + } + + return true; + } + + private async Task LoadWebStages() + { + var clientType = GetClientType(); + + JObject activity = await _maaApiService.RequestMaaApiWithCache(StageApi); + JObject tasksJson = await _maaApiService.RequestMaaApiWithCache(TasksApi); if (clientType != "Official" && tasksJson != null) { - tasksPath = "resource/global/" + clientType + '/' + tasksPath; + var tasksPath = "resource/global/" + clientType + '/' + TasksApi; // Download the client specific resources only when the Official ones are successfully downloaded so that the client specific resource version is the actual version // TODO: There may be an issue when the CN resource is loaded from cache (e.g. network down) while global resource is downloaded (e.g. network up again) // var tasksJsonClient = fromWeb ? WebService.RequestMaaApiWithCache(tasksPath) : WebService.RequestMaaApiWithCache(tasksPath); - _ = _maaApiService.RequestMaaApiWithCache(tasksPath); + await _maaApiService.RequestMaaApiWithCache(tasksPath); } + return activity; + } + + private void UpdateStageInternal(JObject activity) + { + var tempStage = new Dictionary + { + // 这里会被 “剩余理智” 复用,第一个必须是 string.Empty 的 + // 「当前/上次」关卡导航 + { string.Empty, new StageInfo { Display = LocalizationHelper.GetString("DefaultStage"), Value = string.Empty } }, + }; + + var clientType = GetClientType(); + bool isDebugVersion = Marshal.PtrToStringAnsi(AsstGetVersion()) == "DEBUG VERSION"; bool curVerParsed = SemVersion.TryParse(Marshal.PtrToStringAnsi(AsstGetVersion()), SemVersionStyles.AllowLowerV, out var curVersionObj); diff --git a/src/MaaWpfGui/Services/Web/HttpService.cs b/src/MaaWpfGui/Services/Web/HttpService.cs index e84ff016f6..9a6531d472 100644 --- a/src/MaaWpfGui/Services/Web/HttpService.cs +++ b/src/MaaWpfGui/Services/Web/HttpService.cs @@ -1,4 +1,4 @@ -// +// // MaaWpfGui - A part of the MaaCoreArknights project // Copyright (C) 2021 MistEO and Contributors // @@ -163,7 +163,7 @@ namespace MaaWpfGui.Services.Web var success = true; try { - var stream = response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var fileStream = new FileStream(fullFilePathWithTemp, FileMode.Create, FileAccess.Write); // 记录初始化 diff --git a/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs b/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs index 2c874d6c83..ae69001584 100644 --- a/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs @@ -1749,14 +1749,10 @@ namespace MaaWpfGui.ViewModels.UI /// /// Updates manually. /// - // TODO: 你确定要用 async void 不是 async Task? - public async void ManualUpdate() + /// A representing the asynchronous operation. + public async Task ManualUpdate() { - var task = Task.Run(() => - { - return _versionUpdateViewModel.CheckAndDownloadUpdate(true); - }); - var ret = await task; + var ret = await _versionUpdateViewModel.CheckAndDownloadUpdate(true); string toastMessage = null; switch (ret) diff --git a/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs b/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs index 146720a62a..c8bee3681c 100644 --- a/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs @@ -22,7 +22,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; -using System.Windows.Forms; +using System.Windows.Threading; using MaaWpfGui.Constants; using MaaWpfGui.Extensions; using MaaWpfGui.Helper; @@ -171,12 +171,11 @@ namespace MaaWpfGui.ViewModels.UI } */ - private readonly Timer _timer = new Timer(); + private readonly DispatcherTimer _timer = new DispatcherTimer(); private void InitTimer() { - _timer.Enabled = true; - _timer.Interval = 1000 * 50; + _timer.Interval = TimeSpan.FromSeconds(50); _timer.Tick += Timer1_Elapsed; _timer.Start(); } @@ -185,11 +184,14 @@ namespace MaaWpfGui.ViewModels.UI { if (NeedToUpdateDatePrompt()) { - Execute.OnUIThread(() => + Task.Run(async () => { - _stageManager.UpdateStage(true); - UpdateDatePrompt(); - UpdateStageList(false); + await _stageManager.UpdateStageWeb(); + Execute.OnUIThread(() => + { + UpdateDatePrompt(); + UpdateStageList(false); + }); }); } @@ -204,7 +206,7 @@ namespace MaaWpfGui.ViewModels.UI { if (_settingsViewModel.UpdatAutoCheck) { - _settingsViewModel.ManualUpdate(); + Task.Run(_settingsViewModel.ManualUpdate); } } diff --git a/src/MaaWpfGui/ViewModels/UI/VersionUpdateViewModel.cs b/src/MaaWpfGui/ViewModels/UI/VersionUpdateViewModel.cs index 90902a5ad4..be09a1dfa0 100644 --- a/src/MaaWpfGui/ViewModels/UI/VersionUpdateViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/VersionUpdateViewModel.cs @@ -20,6 +20,7 @@ using System.IO.Compression; using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Windows; using System.Windows.Documents; using System.Windows.Input; @@ -357,14 +358,13 @@ namespace MaaWpfGui.ViewModels.UI /// /// 是否强制检查。 /// 操作成功返回 ,反之则返回 - public CheckUpdateRetT CheckAndDownloadUpdate(bool force = false) + public async Task CheckAndDownloadUpdate(bool force = false) { _settingsViewModel.IsCheckingForUpdates = true; - var checkResult = ((Func)(() => - { + async Task CheckUpdateInner() { // 检查更新 - var checkRet = CheckUpdate(force); + var checkRet = await CheckUpdate(force); if (checkRet != CheckUpdateRetT.OK) { return checkRet; @@ -507,7 +507,9 @@ namespace MaaWpfGui.ViewModels.UI } return CheckUpdateRetT.OK; - }))(); + } + + var checkResult = await CheckUpdateInner(); _settingsViewModel.IsCheckingForUpdates = false; return checkResult; @@ -537,7 +539,7 @@ namespace MaaWpfGui.ViewModels.UI /// /// 是否强制检查。 /// 检查到更新返回 ,反之则返回 - private CheckUpdateRetT CheckUpdate(bool force = false) + private async Task CheckUpdate(bool force = false) { // 自动更新或者手动触发 if (!(_settingsViewModel.UpdateCheck || force)) @@ -558,7 +560,7 @@ namespace MaaWpfGui.ViewModels.UI { // 稳定版更新使用主仓库 /latest 接口 // 直接使用 MaaRelease 的话,30 个可能会找不到稳定版,因为有可能 Nightly 发了很多 - var stableResponse = RequestGithubApi(StableRequestUrl, RequestRetryMaxTimes); + var stableResponse = await RequestGithubApi(StableRequestUrl, RequestRetryMaxTimes); if (string.IsNullOrEmpty(stableResponse)) { return CheckUpdateRetT.NetworkError; @@ -566,7 +568,7 @@ namespace MaaWpfGui.ViewModels.UI _latestJson = JsonConvert.DeserializeObject(stableResponse) as JObject; _latestVersion = _latestJson["tag_name"].ToString(); - stableResponse = RequestGithubApi(MaaReleaseRequestUrlByTag + _latestVersion, RequestRetryMaxTimes); + stableResponse = await RequestGithubApi(MaaReleaseRequestUrlByTag + _latestVersion, RequestRetryMaxTimes); // 主仓库能找到版,但是 MaaRelease 找不到,说明 MaaRelease 还没有同步(一般过个十分钟就同步好了) if (string.IsNullOrEmpty(stableResponse)) @@ -579,7 +581,7 @@ namespace MaaWpfGui.ViewModels.UI else { // 非稳定版更新使用 MaaRelease/releases 接口 - var response = RequestGithubApi(RequestUrl, RequestRetryMaxTimes); + var response = await RequestGithubApi(RequestUrl, RequestRetryMaxTimes); if (string.IsNullOrEmpty(response)) { return CheckUpdateRetT.NetworkError; @@ -636,7 +638,7 @@ namespace MaaWpfGui.ViewModels.UI // 非稳定版本是 Nightly 下载的,主仓库没有它的更新信息,不必请求 if (isStdVersion(_latestVersion)) { - var infoResponse = RequestGithubApi(InfoRequestUrl + _latestVersion, RequestRetryMaxTimes); + var infoResponse = await RequestGithubApi(InfoRequestUrl + _latestVersion, RequestRetryMaxTimes); if (string.IsNullOrEmpty(infoResponse)) { return CheckUpdateRetT.FailedToGetInfo; @@ -672,7 +674,7 @@ namespace MaaWpfGui.ViewModels.UI } } - private string RequestGithubApi(string url, int retryTimes) + private async Task RequestGithubApi(string url, int retryTimes) { string response = string.Empty; string[] requestSource = { "https://api.github.com/", "https://api.kgithub.com/" }; @@ -680,7 +682,8 @@ namespace MaaWpfGui.ViewModels.UI { for (var i = 0; i < requestSource.Length; i++) { - response = _httpService.GetStringAsync(new Uri(requestSource[i] + url)).ConfigureAwait(false).GetAwaiter().GetResult(); + // prevent current thread + response = await _httpService.GetStringAsync(new Uri(requestSource[i] + url)).ConfigureAwait(false); if (!string.IsNullOrEmpty(response)) { break;