diff --git a/README.md b/README.md index e72f597eb0..b051b21848 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,8 @@ System administrators will most likely have their own configuration plans, but h Once complete, test that your configuration worked by visiting your proxy site from a browser on a different computer. You should recieve a 401 Unauthorized response. +_NOTE: For SignalR to function properly, make sure your reverse proxy setup supports SSE (Server-Sent Events)_ + #### IIS (Reccommended for Windows) 1. Acquire an HTTPS certificate. The easiet free way for Windows is [win-acme](https://github.com/PKISharp/win-acme) (requires you to set up the website first) diff --git a/build/Version.props b/build/Version.props index 65f6b7e5c8..a0e852bce2 100644 --- a/build/Version.props +++ b/build/Version.props @@ -20,6 +20,6 @@ https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/6.0.24/dotnet-hosting-6.0.24-win.exe 10.11.5 - https://ftp.osuosl.org/pub/mariadb//mariadb-10.11.5/winx64-packages/mariadb-10.11.5-winx64.msi + https://atl.mirrors.knownhost.com/mariadb//mariadb-10.11.5/winx64-packages/mariadb-10.11.5-winx64.msi diff --git a/src/Tgstation.Server.Host/Jobs/JobService.cs b/src/Tgstation.Server.Host/Jobs/JobService.cs index c74204cd8e..87387c60a9 100644 --- a/src/Tgstation.Server.Host/Jobs/JobService.cs +++ b/src/Tgstation.Server.Host/Jobs/JobService.cs @@ -369,6 +369,8 @@ namespace Tgstation.Server.Host.Jobs var hubUpdatesTask = Task.CompletedTask; var result = false; + var firstLogHappened = false; + var hubGroupName = JobsHub.HubGroupName(job); Stopwatch stopwatch = null; void QueueHubUpdate(JobResponse update, bool final) @@ -380,10 +382,16 @@ namespace Tgstation.Server.Host.Jobs { await currentUpdatesTask; + if (!firstLogHappened) + { + logger.LogTrace("Sending updates for job {id} to hub group {group}", update.Id.Value, hubGroupName); + firstLogHappened = true; + } + // DCT: Cancellation token is for job, operation should always run await hub .Clients - .Group(JobsHub.HubGroupName(job)) + .Group(hubGroupName) .ReceiveJobUpdate(update, CancellationToken.None); } diff --git a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs index 719138e6ad..2e2a5da93d 100644 --- a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs +++ b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs @@ -117,6 +117,8 @@ namespace Tgstation.Server.Host.Jobs { ArgumentNullException.ThrowIfNull(authenticationContext); + logger.LogTrace("MapConnectionGroups UID: {uid}", authenticationContext.User.Id.Value); + List permedInstanceIds = null; await databaseContextFactory.UseContext( async databaseContext => @@ -124,7 +126,7 @@ namespace Tgstation.Server.Host.Jobs .InstancePermissionSets .AsQueryable() .Where(ips => ips.PermissionSetId == authenticationContext.PermissionSet.Id.Value) - .Select(ips => ips.Id) + .Select(ips => ips.InstanceId) .ToListAsync(cancellationToken)); await mappingFunc( diff --git a/src/Tgstation.Server.Host/System/IProcess.cs b/src/Tgstation.Server.Host/System/IProcess.cs index aec3f0d7a3..dc9a1d5eac 100644 --- a/src/Tgstation.Server.Host/System/IProcess.cs +++ b/src/Tgstation.Server.Host/System/IProcess.cs @@ -26,7 +26,7 @@ namespace Tgstation.Server.Host.System /// A resulting in the stderr and stdout output of the . /// /// To guarantee that all data is received from the when redirecting streams to a file - /// the result of this function must be ed before is called. + /// the result of this function must be ed before is called. /// Task GetCombinedOutput(CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index ccda2f2153..0bc76ce03f 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -131,7 +131,8 @@ namespace Tgstation.Server.Host.System { if (readTask == null) throw new InvalidOperationException("Output/Error stream reading was not enabled!"); - return readTask; + + return readTask.WaitAsync(cancellationToken); } /// diff --git a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs index 3892aad56c..1f09be3571 100644 --- a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs +++ b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs @@ -85,9 +85,17 @@ namespace Tgstation.Server.Host.Utils.SignalR var mappingTask = OnConnectionMapGroups?.Invoke( authenticationContext, - mappedGroups => Task.WhenAll( - mappedGroups.Select( - group => hub.Groups.AddToGroupAsync(context.ConnectionId, group, cancellationToken))), + mappedGroups => + { + mappedGroups = mappedGroups.ToList(); + logger.LogTrace( + "Mapping connection ID {connectionId} with groups: {mappedGroups}", + context.ConnectionId, + String.Join(", ", mappedGroups)); + return Task.WhenAll( + mappedGroups.Select( + group => hub.Groups.AddToGroupAsync(context.ConnectionId, group, cancellationToken))); + }, cancellationToken) ?? ValueTask.CompletedTask; userConnections.AddOrUpdate( diff --git a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobsHubGroupMapper.cs b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobsHubGroupMapper.cs new file mode 100644 index 0000000000..b00d2af4e8 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobsHubGroupMapper.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Tgstation.Server.Api.Hubs; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Utils.SignalR; + +namespace Tgstation.Server.Host.Tests.Jobs +{ + [TestClass] + public sealed class TestJobsHubGroupMapper + { + [TestMethod] + public async Task TestGroupMapping() + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Trace); + }); + + var mockHub = new Mock>(); + var mockDcf = new Mock(); + + + using var context = Utils.CreateDatabaseContext(); + mockDcf.Setup(x => x.UseContext(It.IsNotNull>())).Returns>(func => func(context)); + + var mockPs = new PermissionSet + { + Id = 23421, + InstanceManagerRights = RightsHelper.AllRights(), + AdministrationRights = RightsHelper.AllRights(), + }; + var testIps1 = new InstancePermissionSet + { + ByondRights = RightsHelper.AllRights(), + ChatBotRights = RightsHelper.AllRights(), + ConfigurationRights = RightsHelper.AllRights(), + DreamDaemonRights = RightsHelper.AllRights(), + DreamMakerRights = RightsHelper.AllRights(), + Id = 43892849, + InstanceId = 348928, + InstancePermissionSetRights = RightsHelper.AllRights(), + RepositoryRights = RightsHelper.AllRights(), + PermissionSetId = mockPs.Id.Value, + PermissionSet = mockPs, + }; + + var testIps2 = new InstancePermissionSet + { + ByondRights = RightsHelper.AllRights(), + ChatBotRights = RightsHelper.AllRights(), + ConfigurationRights = RightsHelper.AllRights(), + DreamDaemonRights = RightsHelper.AllRights(), + DreamMakerRights = RightsHelper.AllRights(), + Id = 454354, + InstanceId = 2234, + InstancePermissionSetRights = RightsHelper.AllRights(), + RepositoryRights = RightsHelper.AllRights(), + PermissionSetId = mockPs.Id.Value, + PermissionSet = mockPs, + }; + context.InstancePermissionSets.Add(testIps1); + context.InstancePermissionSets.Add(testIps2); + + var cancellationToken = CancellationToken.None; + await context.SaveChangesAsync(cancellationToken); + + var mockUpdater = new Mock(); + + var mapper = new JobsHubGroupMapper( + mockHub.Object, + mockDcf.Object, + mockUpdater.Object, + loggerFactory.CreateLogger()); + + await mapper.StartAsync(cancellationToken); + + var mockAuthenticationContext = new Mock(); + var mockUser = new User + { + Id = 2134134, + }; + + mockAuthenticationContext.SetupGet(x => x.User).Returns(mockUser); + + mockAuthenticationContext.SetupGet(x => x.PermissionSet).Returns(mockPs); + + bool ran = false; + Task Callback(IEnumerable results) + { + ran = true; + Assert.AreEqual(2, results.Count()); + Assert.IsTrue(results.Contains(JobsHub.HubGroupName(testIps1.InstanceId))); + Assert.IsTrue(results.Contains(JobsHub.HubGroupName(testIps2.InstanceId))); + return Task.CompletedTask; + } + + await mockHub.RaiseAsync(x => x.OnConnectionMapGroups += null, mockAuthenticationContext.Object, (Func, Task>)Callback, cancellationToken); + + Assert.IsTrue(ran); + + await mapper.StopAsync(cancellationToken); + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests/MemoryDatabaseContext.cs b/tests/Tgstation.Server.Host.Tests/MemoryDatabaseContext.cs new file mode 100644 index 0000000000..0bc4f8e300 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/MemoryDatabaseContext.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; + +using Tgstation.Server.Host.Database; + +namespace Tgstation.Server.Host.Tests +{ + sealed class MemoryDatabaseContext : DatabaseContext + { + public MemoryDatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) + { + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj index f921ae5bf0..f0f88a74b5 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -5,6 +5,10 @@ $(TgsFrameworkVersion) + + + + diff --git a/tests/Tgstation.Server.Host.Tests/Utils.cs b/tests/Tgstation.Server.Host.Tests/Utils.cs new file mode 100644 index 0000000000..3700bff8d6 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Utils.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; + +using Tgstation.Server.Host.Database; + +namespace Tgstation.Server.Host.Tests +{ + static class Utils + { + public static MemoryDatabaseContext CreateDatabaseContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "TgsTestDB") + .Options; + return new MemoryDatabaseContext(options); + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs index 8875a9b5ff..330df08e6b 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs @@ -229,13 +229,6 @@ namespace Tgstation.Server.Tests.Live.Instance // force token refreshs await Task.WhenAll(permedUser.Administration.Read(cancellationToken).AsTask(), permlessUser.Instances.List(null, cancellationToken).AsTask()); - await Task.WhenAll(conn1.StartAsync(cancellationToken), conn2.StartAsync(cancellationToken)); - - Assert.AreEqual(HubConnectionState.Connected, conn1.State); - Assert.AreEqual(HubConnectionState.Connected, conn2.State); - Console.WriteLine($"New conn1: {conn1.ConnectionId}"); - Console.WriteLine($"New conn2: {conn2.ConnectionId}"); - if (!permlessPsId.HasValue) { var permlessUserId = long.Parse(permlessUser.Token.ParseJwt().Subject); @@ -267,6 +260,13 @@ namespace Tgstation.Server.Tests.Live.Instance PermissionSetId = permlessPsId.Value }, cancellationToken); })); + + await Task.WhenAll(conn1.StartAsync(cancellationToken), conn2.StartAsync(cancellationToken)); + + Assert.AreEqual(HubConnectionState.Connected, conn1.State); + Assert.AreEqual(HubConnectionState.Connected, conn2.State); + Console.WriteLine($"New conn1: {conn1.ConnectionId}"); + Console.WriteLine($"New conn2: {conn2.ConnectionId}"); } public void CompleteNow() => finishTcs.TrySetResult();