diff --git a/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs index 4bfeae9..2e9fc66 100644 --- a/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs @@ -451,6 +451,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase { _zoningCts?.CancelAfter(2500); } + catch (ObjectDisposedException) + { + // ignore + } catch (Exception ex) { Logger.LogWarning(ex, "Zoning CTS cancel issue"); diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index e6a9a89..93ade55 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -86,7 +86,7 @@ public record CombatOrPerformanceEndMessage : MessageBase; public record EventMessage(Event Event) : MessageBase; public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBase; public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage; -public record GameObjectHandlerCreatedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : MessageBase; -public record GameObjectHandlerDestroyedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : MessageBase; +public record GameObjectHandlerCreatedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : SameThreadMessage; +public record GameObjectHandlerDestroyedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : SameThreadMessage; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 378b00c..3577d76 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -23,14 +23,13 @@ using MareSynchronos.WebAPI.Files; using MareSynchronos.WebAPI.Files.Models; using MareSynchronos.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; using System.Collections.Concurrent; +using System.Diagnostics; using System.Globalization; using System.Net.Http.Json; using System.Numerics; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; namespace MareSynchronos.UI; @@ -121,6 +120,9 @@ public class SettingsUi : WindowMediatorSubscriberBase public override void OnOpen() { _uiShared.RestOAuthTasksState(); + _speedTestCts?.Cancel(); + _speedTestCts?.Dispose(); + _speedTestCts = new(); } public override void OnClose() @@ -128,6 +130,10 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.EditTrackerPosition = false; _uidToAddForIgnore = string.Empty; _secretKeysConversionCts = _secretKeysConversionCts.CancelRecreate(); + _downloadServersTask = null; + _speedTestTask = null; + _speedTestCts?.Cancel(); + _speedTestCts?.Dispose(); base.OnClose(); } @@ -333,6 +339,62 @@ public class SettingsUi : WindowMediatorSubscriberBase if (!showUploading) ImGui.EndDisabled(); if (!showTransferBars) ImGui.EndDisabled(); + if (_apiController.IsConnected) + { + ImGuiHelpers.ScaledDummy(10); + using var tree = ImRaii.TreeNode("Speed Test to Servers"); + if (tree) + { + if (_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) && (!_downloadServersTask?.IsCompletedSuccessfully ?? false))) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.GroupArrowsRotate, "Update Download Server List")) + { + _downloadServersTask = GetDownloadServerList(); + } + } + if (_downloadServersTask != null && _downloadServersTask.IsCompleted && !_downloadServersTask.IsCompletedSuccessfully) + { + UiSharedService.ColorTextWrapped("Failed to get download servers from service, see /xllog for more information", ImGuiColors.DalamudRed); + } + if (_downloadServersTask != null && _downloadServersTask.IsCompleted && _downloadServersTask.IsCompletedSuccessfully) + { + if (_speedTestTask == null || _speedTestTask.IsCompleted) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight, "Start Speedtest")) + { + _speedTestTask = RunSpeedTest(_downloadServersTask.Result!, _speedTestCts.Token); + } + } + else if (!_speedTestTask.IsCompleted) + { + UiSharedService.ColorTextWrapped("Running Speedtest to File Servers...", ImGuiColors.DalamudYellow); + UiSharedService.ColorTextWrapped("Please be patient, depending on usage and load this can take a while.", ImGuiColors.DalamudYellow); + if (_uiShared.IconTextButton(FontAwesomeIcon.Ban, "Cancel speedtest")) + { + _speedTestCts.Cancel(); + _speedTestCts.Dispose(); + _speedTestCts = new(); + } + } + if (_speedTestTask != null && _speedTestTask.IsCompleted) + { + if (_speedTestTask.Result != null && _speedTestTask.Result.Count != 0) + { + foreach (var result in _speedTestTask.Result) + { + UiSharedService.TextWrapped(result); + } + } + else + { + UiSharedService.ColorTextWrapped("Speedtest completed with no results", ImGuiColors.DalamudYellow); + } + } + } + } + ImGuiHelpers.ScaledDummy(10); + } + ImGui.Separator(); _uiShared.BigText("Current Transfers"); @@ -410,6 +472,87 @@ public class SettingsUi : WindowMediatorSubscriberBase } } + private Task?>? _downloadServersTask = null; + private Task?>? _speedTestTask = null; + private CancellationTokenSource _speedTestCts = new(); + + private async Task?> RunSpeedTest(List servers, CancellationToken token) + { + List speedTestResults = new(); + foreach (var server in servers) + { + HttpResponseMessage? result = null; + Stopwatch? st = null; + try + { + result = await _fileTransferOrchestrator.SendRequestAsync(HttpMethod.Get, new Uri(new Uri(server), "speedtest/run"), token, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + result.EnsureSuccessStatusCode(); + using CancellationTokenSource speedtestTimeCts = new(); + speedtestTimeCts.CancelAfter(TimeSpan.FromSeconds(10)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(speedtestTimeCts.Token, token); + long readBytes = 0; + st = Stopwatch.StartNew(); + try + { + var stream = await result.Content.ReadAsStreamAsync(linkedCts.Token).ConfigureAwait(false); + byte[] buffer = new byte[8192]; + while (!speedtestTimeCts.Token.IsCancellationRequested) + { + var currentBytes = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(false); + if (currentBytes == 0) + break; + readBytes += currentBytes; + } + } + catch (OperationCanceledException) + { + _logger.LogWarning("Speedtest to {server} cancelled", server); + } + st.Stop(); + _logger.LogInformation("Downloaded {bytes} from {server} in {time}", UiSharedService.ByteToString(readBytes), server, st.Elapsed); + var bps = (long)((readBytes) / st.Elapsed.TotalSeconds); + speedTestResults.Add($"{server}: ~{UiSharedService.ByteToString(bps)}/s"); + } + catch (HttpRequestException ex) + { + if (result != null) + { + var res = await result!.Content.ReadAsStringAsync().ConfigureAwait(false); + speedTestResults.Add($"{server}: {ex.Message} - {res}"); + } + } + catch (OperationCanceledException) + { + _logger.LogWarning("Speedtest on {server} cancelled", server); + speedTestResults.Add($"{server}: Cancelled by user"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Some exception"); + } + finally + { + st?.Stop(); + } + } + return speedTestResults; + } + + private async Task?> GetDownloadServerList() + { + try + { + var result = await _fileTransferOrchestrator.SendRequestAsync(HttpMethod.Get, new Uri(_fileTransferOrchestrator.FilesCdnUri!, "files/downloadServers"), CancellationToken.None).ConfigureAwait(false); + result.EnsureSuccessStatusCode(); + return await JsonSerializer.DeserializeAsync>(await result.Content.ReadAsStreamAsync().ConfigureAwait(false)).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to get download server list"); + throw; + } + } + private void DrawDebug() { _lastTab = "Debug"; diff --git a/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs index 3732168..26dc967 100644 --- a/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs +++ b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs @@ -168,6 +168,10 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase return await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false); return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false); } + catch (TaskCanceledException) + { + throw; + } catch (Exception ex) { Logger.LogWarning(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri);