diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 82c4a94..b0f2710 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -1423,7 +1423,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa 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 +1577,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(5)); + 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); 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 { diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index 2731619..8aa2b0b 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -502,6 +502,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } } + private void RemoveStatus(string key) + { + lock (_downloadStatusLock) + { + _downloadStatus.Remove(key); + } + } + private async Task DecompressBlockFileAsync( string downloadStatusKey, string blockFilePath, @@ -595,6 +603,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase { Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel); } + finally + { + RemoveStatus(downloadStatusKey); + } } public async Task> InitiateDownloadList( @@ -866,6 +878,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1); Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash); + + RemoveStatus(directDownload.DirectDownloadUrl!); } catch (OperationCanceledException ex) {