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();