diff --git a/build/ControlPanelVersion.props b/build/ControlPanelVersion.props index f6f147afdc..02d75e7142 100644 --- a/build/ControlPanelVersion.props +++ b/build/ControlPanelVersion.props @@ -1,6 +1,6 @@ - 4.26.0 + 4.26.2 diff --git a/build/Version.props b/build/Version.props index 78ae02ffda..e3a0b04745 100644 --- a/build/Version.props +++ b/build/Version.props @@ -8,7 +8,7 @@ 10.0.0 7.0.0 13.0.0 - 16.0.0 + 15.0.0 6.6.2 5.6.2 1.4.0 diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index 3573822c52..e499846add 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 28c5611bdd..4f6be11b5a 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -372,13 +372,15 @@ namespace Tgstation.Server.Client retryPolicy ??= new InfiniteThirtySecondMaxRetryPolicy(); + var wrappedPolicy = new ApiClientTokenRefreshRetryPolicy(this, retryPolicy); + HubConnection? hubConnection = null; var hubConnectionBuilder = new HubConnectionBuilder() .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings = SerializerSettings; }) - .WithAutomaticReconnect(retryPolicy) + .WithAutomaticReconnect(wrappedPolicy) .WithUrl( new Uri(Url, Routes.JobsHub), HttpTransportType.ServerSentEvents, @@ -480,6 +482,9 @@ namespace Tgstation.Server.Client if (content == null && (method == HttpMethod.Post || method == HttpMethod.Put)) throw new InvalidOperationException("content cannot be null for POST or PUT!"); + if (disposed) + throw new ObjectDisposedException(nameof(ApiClient)); + HttpResponseMessage response; var fullUri = new Uri(Url, route); var serializerSettings = SerializerSettings; diff --git a/src/Tgstation.Server.Client/ApiClientTokenRefreshRetryPolicy.cs b/src/Tgstation.Server.Client/ApiClientTokenRefreshRetryPolicy.cs new file mode 100644 index 0000000000..34556c24ec --- /dev/null +++ b/src/Tgstation.Server.Client/ApiClientTokenRefreshRetryPolicy.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; + +using Microsoft.AspNetCore.SignalR.Client; + +namespace Tgstation.Server.Client +{ + /// + /// A that attempts to refresh a given 's token on the first disconnect. + /// + sealed class ApiClientTokenRefreshRetryPolicy : IRetryPolicy + { + /// + /// The backing . + /// + readonly ApiClient apiClient; + + /// + /// The wrapped . + /// + readonly IRetryPolicy wrappedPolicy; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public ApiClientTokenRefreshRetryPolicy(ApiClient apiClient, IRetryPolicy wrappedPolicy) + { + this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); + this.wrappedPolicy = wrappedPolicy ?? throw new ArgumentNullException(nameof(wrappedPolicy)); + } + + /// + public TimeSpan? NextRetryDelay(RetryContext retryContext) + { + if (retryContext == null) + throw new ArgumentNullException(nameof(retryContext)); + + if (retryContext.PreviousRetryCount == 0) + AttemptTokenRefresh(); + + return wrappedPolicy.NextRetryDelay(retryContext); + } + + /// + /// Attempt to refresh the s token asynchronously. + /// + async void AttemptTokenRefresh() + { + try + { + await apiClient.RefreshToken(CancellationToken.None); + } + catch + { + // intentionally ignored + } + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 42869e8a0e..d730b9de10 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -403,7 +403,9 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(provider => provider.GetRequiredService()); services.AddSingleton(provider => provider.GetRequiredService()); services.AddSingleton(x => x.GetRequiredService()); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(provider => provider.GetRequiredService()); + services.AddSingleton(x => x.GetRequiredService()); // bit of a hack, but we need this to load immediated services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); diff --git a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs index 77ccf99b0b..719138e6ad 100644 --- a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs +++ b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Hubs; @@ -19,7 +20,7 @@ namespace Tgstation.Server.Host.Jobs /// /// Handles mapping groups for the . /// - sealed class JobsHubGroupMapper : IPermissionsUpdateNotifyee + sealed class JobsHubGroupMapper : IPermissionsUpdateNotifyee, IHostedService { /// /// The for the . @@ -96,6 +97,12 @@ namespace Tgstation.Server.Host.Jobs cancellationToken); } + /// + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + /// /// Implementation of . /// diff --git a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs index 7999908052..3892aad56c 100644 --- a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs +++ b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs @@ -83,12 +83,13 @@ namespace Tgstation.Server.Host.Utils.SignalR userId, context.ConnectionId); - var mappingTask = OnConnectionMapGroups( + var mappingTask = OnConnectionMapGroups?.Invoke( authenticationContext, mappedGroups => Task.WhenAll( mappedGroups.Select( group => hub.Groups.AddToGroupAsync(context.ConnectionId, group, cancellationToken))), - cancellationToken); + cancellationToken) + ?? ValueTask.CompletedTask; userConnections.AddOrUpdate( userId, _ => new Dictionary