From 94f520d0e7ec14500581704e9ccbeb8faa4899f2 Mon Sep 17 00:00:00 2001 From: defnotken Date: Sun, 28 Dec 2025 15:13:40 +0000 Subject: [PATCH 1/7] Add Serious Warning about nameplates (#118) Co-authored-by: defnotken Reviewed-on: https://git.lightless-sync.org/Lightless-Sync/LightlessClient/pulls/118 Co-authored-by: defnotken Co-committed-by: defnotken --- LightlessSync/UI/SettingsUi.cs | 49 +++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index a0c1787..1c86580 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -84,6 +84,8 @@ public class SettingsUi : WindowMediatorSubscriberBase private bool _pairDiagnosticsEnabled; private string? _selectedPairDebugUid = null; private string _lightfinderIconInput = string.Empty; + private bool _showLightfinderRendererWarning = false; + private LightfinderLabelRenderer _pendingLightfinderRenderer = LightfinderLabelRenderer.Pictomancy; private bool _lightfinderIconInputInitialized = false; private int _lightfinderIconPresetIndex = -1; private static readonly LightlessConfig DefaultConfig = new(); @@ -2372,7 +2374,7 @@ public class SettingsUi : WindowMediatorSubscriberBase var labelRenderer = _configService.Current.LightfinderLabelRenderer; var labelRendererLabel = labelRenderer switch { - LightfinderLabelRenderer.SignatureHook => "Native nameplate (sig hook)", + LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering", _ => "ImGui Overlay", }; @@ -2382,18 +2384,25 @@ public class SettingsUi : WindowMediatorSubscriberBase { var optionLabel = option switch { - LightfinderLabelRenderer.SignatureHook => "Native Nameplate (sig hook)", + LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering", _ => "ImGui Overlay", }; var selected = option == labelRenderer; if (ImGui.Selectable(optionLabel, selected)) { - _configService.Current.LightfinderLabelRenderer = option; - _configService.Save(); - _nameplateService.RequestRedraw(); + if (option == LightfinderLabelRenderer.SignatureHook) + { + _pendingLightfinderRenderer = option; + _showLightfinderRendererWarning = true; + } + else + { + _configService.Current.LightfinderLabelRenderer = option; + _configService.Save(); + _nameplateService.RequestRedraw(); + } } - if (selected) ImGui.SetItemDefaultFocus(); } @@ -2401,6 +2410,34 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndCombo(); } + if (_showLightfinderRendererWarning) + { + ImGui.SetNextWindowSize(new Vector2(450f, 0f), ImGuiCond.Appearing); + ImGui.OpenPopup("Nameplate Warning"); + } + + if (ImGui.BeginPopupModal("Nameplate Warning", ref _showLightfinderRendererWarning, ImGuiWindowFlags.AlwaysAutoResize)) + { + ImGui.TextColored(UIColors.Get("DimRed"), "USE AT YOUR RISK!"); + ImGui.Spacing(); + ImGui.TextWrapped("Writing on to the native Nameplates is known to be unstable and MAY cause crashes. DO NOT REPORT THOSE CRASHES TO DALAMUD. We will also not be supporting Nameplate crashes. You have been warned."); + ImGui.Spacing(); + ImGui.TextWrapped("By accepting this warning, you understand that you are using this feature at risk of crashing."); + ImGui.Spacing(); + + var buttonWidth = ImGui.GetContentRegionAvail().X; + if (ImGui.Button("I Understand", new Vector2(buttonWidth, 0))) + { + _configService.Current.LightfinderLabelRenderer = _pendingLightfinderRenderer; + _configService.Save(); + _nameplateService.RequestRedraw(); + _showLightfinderRendererWarning = false; + ImGui.CloseCurrentPopup(); + } + + ImGui.EndPopup(); + } + _uiShared.DrawHelpText("Choose how Lightfinder labels render: the default ImGui overlay or native nameplate nodes via signature hook."); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); From 08050614da205e1fdfcd25779aae80d1f3744c71 Mon Sep 17 00:00:00 2001 From: cake Date: Sun, 28 Dec 2025 16:28:27 +0100 Subject: [PATCH 2/7] Own profiles are shown as online now. --- LightlessSync/Services/UiFactory.cs | 3 ++- LightlessSync/UI/StandaloneProfileUi.cs | 28 ++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/LightlessSync/Services/UiFactory.cs b/LightlessSync/Services/UiFactory.cs index 9b90830..33ab3ae 100644 --- a/LightlessSync/Services/UiFactory.cs +++ b/LightlessSync/Services/UiFactory.cs @@ -105,6 +105,7 @@ public class UiFactory groupData: groupData, isLightfinderContext: isLightfinderContext, lightfinderCid: lightfinderCid, - performanceCollector: _performanceCollectorService); + performanceCollector: _performanceCollectorService, + _apiController); } } diff --git a/LightlessSync/UI/StandaloneProfileUi.cs b/LightlessSync/UI/StandaloneProfileUi.cs index d1ebdbe..c33c522 100644 --- a/LightlessSync/UI/StandaloneProfileUi.cs +++ b/LightlessSync/UI/StandaloneProfileUi.cs @@ -11,6 +11,7 @@ using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Services; using LightlessSync.UI.Tags; using LightlessSync.Utils; +using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using System.Numerics; @@ -22,6 +23,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase private readonly PairUiService _pairUiService; private readonly ServerConfigurationManager _serverManager; private readonly ProfileTagService _profileTagService; + private readonly ApiController _apiController; private readonly UiSharedService _uiSharedService; private readonly UserData? _userData; private readonly GroupData? _groupData; @@ -60,7 +62,8 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase GroupData? groupData, bool isLightfinderContext, string? lightfinderCid, - PerformanceCollectorService performanceCollector) + PerformanceCollectorService performanceCollector, + ApiController apiController) : base(logger, mediator, BuildWindowTitle( userData, groupData, @@ -94,6 +97,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase .Apply(); IsOpen = true; + _apiController = apiController; } public Pair? Pair { get; } @@ -248,19 +252,33 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase ResetBannerTexture(); _lastBannerPicture = bannerBytes; } - string? noteText = null; - string statusLabel = _isLightfinderContext ? "Exploring" : "Offline"; + + var isSelfProfile = !_isLightfinderContext + && _userData is not null + && !string.IsNullOrEmpty(_apiController.UID) + && string.Equals(_userData.UID, _apiController.UID, StringComparison.Ordinal); + + string statusLabel = _isLightfinderContext + ? "Exploring" + : isSelfProfile ? "Online" : "Offline"; + string? visiblePlayerName = null; bool directPair = false; bool youPaused = false; bool theyPaused = false; List syncshellLines = []; + if (!_isLightfinderContext) + { + noteText = _serverManager.GetNoteForUid(_userData!.UID); + } + if (!_isLightfinderContext && Pair != null) { var snapshot = _pairUiService.GetSnapshot(); noteText = _serverManager.GetNoteForUid(Pair.UserData.UID); + statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline"); visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null; @@ -282,11 +300,15 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo) ? groupInfo.GroupAliasOrGID : gid; + var groupNote = _serverManager.GetNoteForGid(gid); syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})"); } } } + + if (isSelfProfile) + statusLabel = "Online"; } var presenceTokens = new List From 6d20995dbf00b4485c66672eff3a67e83bc6c1ac Mon Sep 17 00:00:00 2001 From: cake Date: Mon, 29 Dec 2025 02:50:49 +0100 Subject: [PATCH 3/7] Added decompression gate to decompress files --- .../WebAPI/Files/FileDownloadManager.cs | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index 47774f7..2731619 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -28,6 +28,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase private readonly TextureMetadataHelper _textureMetadataHelper; private readonly ConcurrentDictionary _activeDownloadStreams; + private readonly SemaphoreSlim _decompressGate = + new(Math.Max(1, Environment.ProcessorCount / 2), Math.Max(1, Environment.ProcessorCount / 2)); private volatile bool _disableDirectDownloads; private int _consecutiveDirectDownloadFailures; @@ -522,32 +524,57 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase try { + // sanity check length if (fileLengthBytes < 0 || fileLengthBytes > int.MaxValue) throw new InvalidDataException($"Invalid block entry length: {fileLengthBytes}"); + // safe cast after check + var len = checked((int)fileLengthBytes); + if (!replacementLookup.TryGetValue(fileHash, out var repl)) { Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash); - // still need to skip bytes: - var skip = checked((int)fileLengthBytes); - fileBlockStream.Position += skip; + fileBlockStream.Seek(len, SeekOrigin.Current); continue; } + // decompress var filePath = _fileDbManager.GetCacheFilePath(fileHash, repl.Extension); + Logger.LogTrace("{dlName}: Decompressing {file}:{len} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath); - Logger.LogDebug("{dlName}: Decompressing {file}:{len} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath); - - var len = checked((int)fileLengthBytes); + // read compressed data var compressed = new byte[len]; - await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false); - MungeBuffer(compressed); - var decompressed = LZ4Wrapper.Unwrap(compressed); + if (len == 0) + { + await _fileCompactor.WriteAllBytesAsync(filePath, Array.Empty(), ct).ConfigureAwait(false); + PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale); + continue; + } - await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false); - PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale); + MungeBuffer(compressed); + + // limit concurrent decompressions + await _decompressGate.WaitAsync(ct).ConfigureAwait(false); + try + { + var sw = System.Diagnostics.Stopwatch.StartNew(); + + // decompress + var decompressed = LZ4Wrapper.Unwrap(compressed); + + Logger.LogTrace("{dlName}: Unwrap {fileHash} took {ms}ms (compressed {c} bytes, decompressed {d} bytes)", + downloadLabel, fileHash, sw.ElapsedMilliseconds, compressed.Length, decompressed?.Length ?? -1); + + // write to file + await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false); + PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale); + } + finally + { + _decompressGate.Release(); + } } catch (EndOfStreamException) { @@ -605,20 +632,16 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase .. await FilesGetSizes(hashes, ct).ConfigureAwait(false), ]; - Logger.LogDebug("Files with size 0 or less: {files}", - string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); - foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) { if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal))) _orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); } - CurrentDownloads = downloadFileInfoFromService + CurrentDownloads = [.. downloadFileInfoFromService .Distinct() .Select(d => new DownloadFileTransfer(d)) - .Where(d => d.CanBeTransferred) - .ToList(); + .Where(d => d.CanBeTransferred)]; return CurrentDownloads; } From 6b49c92ef98c0020aaa8b5dc873e40deeba6d9d1 Mon Sep 17 00:00:00 2001 From: defnotken Date: Mon, 29 Dec 2025 08:41:32 -0600 Subject: [PATCH 4/7] Add a timeout to prevent deadlock of application data --- .../PlayerData/Pairs/PairHandlerAdapter.cs | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 82c4a94..bec8322 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -1420,10 +1420,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa } private Task? _pairDownloadTask; - private Task _visibilityGraceTask; private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, - bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) + bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) { var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); try @@ -1577,24 +1576,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa RecordFailure("Handler not available for application", "HandlerUnavailable"); return; } + _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); - var appToken = _applicationCancellationTokenSource?.Token; - while ((!_applicationTask?.IsCompleted ?? false) - && !downloadToken.IsCancellationRequested - && (!appToken?.IsCancellationRequested ?? false)) + if (_applicationTask != null && !_applicationTask.IsCompleted) { - Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName); - await Task.Delay(250).ConfigureAwait(false); + Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName); + + var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token); + + try + { + await _applicationTask.WaitAsync(combinedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Logger.LogWarning("[BASE-{appBase}] Timeout waiting for application task {id} to complete, proceeding anyway", applicationBase, _applicationId); + } + finally + { + timeoutCts.Dispose(); + combinedCts.Dispose(); + } } - if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) + if (downloadToken.IsCancellationRequested) { _forceFullReapply = true; RecordFailure("Application cancelled", "Cancellation"); return; } - _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); var token = _applicationCancellationTokenSource.Token; _applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token); From 27d4da4615c57df034d7321739944c2d0707eebc Mon Sep 17 00:00:00 2001 From: defnotken Date: Mon, 29 Dec 2025 08:47:51 -0600 Subject: [PATCH 5/7] thought a variable was unused. --- LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index bec8322..71cdda7 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -1420,6 +1420,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa } private Task? _pairDownloadTask; + private Task _visibilityGraceTask; private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) From 308c2207359c8e6c0d90b81a06b98f9782012817 Mon Sep 17 00:00:00 2001 From: cake Date: Tue, 30 Dec 2025 02:08:54 +0100 Subject: [PATCH 6/7] Fixed auto prune options locked --- LightlessSync/UI/SyncshellAdminUI.cs | 50 +++++++++++++++------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 0458c05..526b5ae 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -116,7 +116,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var drawList = ImGui.GetWindowDrawList(); var purple = UIColors.Get("LightlessPurple"); - var gradLeft = purple.WithAlpha(0.0f); + var gradLeft = purple.WithAlpha(0.0f); var gradRight = purple.WithAlpha(0.85f); uint colTopLeft = ImGui.ColorConvertFloat4ToU32(gradLeft); @@ -162,7 +162,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var subtitlePos = new Vector2( pMin.X + 12f * scale, - titlePos.Y + titleHeight - 2f * scale); + titlePos.Y + titleHeight - 2f * scale); ImGui.SetCursorScreenPos(subtitlePos); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); @@ -392,25 +392,27 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } UiSharedService.AttachToolTip("When enabled, inactive non-pinned, non-moderator users will be pruned automatically on the server."); - ImGui.SameLine(); - ImGui.SetNextItemWidth(150); - - using (ImRaii.Disabled(!_autoPruneEnabled)) - { - _uiSharedService.DrawCombo( - "Day(s) of inactivity", - [1, 3, 7, 14, 30, 90], - days => $"{days} day(s)", - selected => - { - _autoPruneDays = selected; - SavePruneSettings(); - }, - _autoPruneDays); - } if (!_autoPruneEnabled) { + ImGui.BeginDisabled(); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(150); + _uiSharedService.DrawCombo( + "Day(s) of inactivity (gets checked hourly)", + [0, 1, 3, 7, 14, 30, 90], + (count) => count == 0 ? "2 hours(s)" : count + " day(s)", + selected => + { + _autoPruneDays = selected; + SavePruneSettings(); + }, + _autoPruneDays); + + if (!_autoPruneEnabled) + { + ImGui.EndDisabled(); UiSharedService.ColorTextWrapped( "Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.", ImGuiColors.DalamudGrey); @@ -593,7 +595,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase _uiSharedService.DrawCombo( "Day(s) of inactivity", [0, 1, 3, 7, 14, 30, 90], - (count) => count == 0 ? "15 minute(s)" : count + " day(s)", + (count) => count == 0 ? "2 hours(s)" : count + " day(s)", (selected) => { _pruneDays = selected; @@ -663,8 +665,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var style = ImGui.GetStyle(); float fullW = ImGui.GetContentRegionAvail().X; - float colIdentity = fullW * 0.45f; - float colMeta = fullW * 0.35f; + float colIdentity = fullW * 0.45f; + float colMeta = fullW * 0.35f; float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f; // Header @@ -873,7 +875,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var boolcolor = UiSharedService.GetBoolColor(pair.IsOnline); UiSharedService.ColorText(text, boolcolor); - + if (ImGui.IsItemClicked()) ImGui.SetClipboardText(pair.UserData.AliasOrUID); @@ -1093,6 +1095,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); } + private void SavePruneSettings() { if (_autoPruneDays <= 0) @@ -1100,8 +1103,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase _autoPruneEnabled = false; } - var enabled = _autoPruneEnabled && _autoPruneDays > 0; - var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: enabled, AutoPruneDays: enabled ? _autoPruneDays : 0); + var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: _autoPruneEnabled, AutoPruneDays: _autoPruneDays); try { From 9ea0571e825254e71bb7e4e4df1b24989bc0c943 Mon Sep 17 00:00:00 2001 From: defnotken Date: Tue, 30 Dec 2025 14:29:38 +0000 Subject: [PATCH 7/7] Lower Time out --- LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 71cdda7..b0f2710 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -1583,7 +1583,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName); - var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token); try