diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs index 1a912d60d8..4ce5adab21 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs @@ -3,7 +3,7 @@ /// /// A response to a bridge request. /// - public sealed class BridgeResponse : DMApiResponse + public class BridgeResponse : DMApiResponse { /// /// The new port for requests. diff --git a/tests/DMAPI/LongRunning/Config.dm b/tests/DMAPI/LongRunning/Config.dm index 1fff7f2a61..a7dfd4936a 100644 --- a/tests/DMAPI/LongRunning/Config.dm +++ b/tests/DMAPI/LongRunning/Config.dm @@ -6,6 +6,6 @@ #define TGS_WORLD_ANNOUNCE(message) world << ##message #define TGS_INFO_LOG(message) world.log << "Info: [##message]" #define TGS_WARNING_LOG(message) world.log << "Warn: [##message]" -#define TGS_ERROR_LOG(message) world.log << "Err: [##message]" +#define TGS_ERROR_LOG(message) TgsError(##message) #define TGS_NOTIFY_ADMINS(event) #define TGS_CLIENT_COUNT 0 diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index 6ab061b10c..bbd06bfd27 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -49,11 +49,18 @@ world.TgsChatBroadcast(response) world.TgsInitializationComplete() + startup_complete = TRUE + if(run_bridge_test) + CheckBridgeLimits() + /world/Topic(T, Addr, Master, Keys) log << "Topic: [T]" . = HandleTopic(T) log << "Response: [.]" +var/startup_complete +var/run_bridge_test + /world/proc/HandleTopic(T) TGS_TOPIC @@ -63,6 +70,40 @@ RebootAsync() return "ack" + var/tactics2 = data["tgs_integration_test_tactics2"] + if(tactics2) + if(startup_complete) + CheckBridgeLimits() + else + run_bridge_test = TRUE + return "ack2" + + // Topic limit tests + // Receive + var/tactics3 = data["tgs_integration_test_tactics3"] + if(tactics3) + var/list/json = json_decode(tactics3) + if(!json || !istext(json["payload"]) || !istext(json["size"])) + return "fail" + + var/size = text2num(json["size"]) + var/payload = json["payload"] + if(length(payload) != size) + return "fail" + + return "pass" + + // Send + var/tactics4 = data["tgs_integration_test_tactics4"] + if(tactics4) + var/size = isnum(tactics4) ? tactics4 : text2num(tactics4) + if(!isnum(size)) + text2file("tgs_integration_test_tactics4 wasn't a number!", "test_fail_reason.txt") + del(world) + + var/payload = create_payload(size) + return payload + TgsChatBroadcast("Recieved non-tgs topic: [T]") return "feck" @@ -77,7 +118,8 @@ world.TgsChatBroadcast("Recieved event: `[json_encode(args)]`") /world/Export(url) - log << "Export: [url]" + if(length(url) < 1000) + log << "Export: [url]" return ..() /proc/RebootAsync() @@ -113,3 +155,60 @@ response.embed.fields += field response.embed.footer = new /datum/tgs_chat_embed/footer("Footer text") return response + +var/lastTgsError + +/proc/TgsError(message) + world.log << "Err: [message]" + lastTgsError = message + +/proc/create_payload(size) + var/builder = list() + for(var/j = 0; j < size; ++j) + builder += "a" + var/payload = jointext(builder, "") + return payload + +/proc/CheckBridgeLimits() + set waitfor = FALSE + CheckBridgeLimitsImpl() + +/proc/CheckBridgeLimitsImpl() + sleep(30) + + // Evil custom bridge command hacking here + var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs) + var/old_ai = api.access_identifier + api.access_identifier = "tgs_integration_test" + + // Always send chat messages because they can have extremely large payloads with the text + + // bisecting request test + var/base = 1 + var/nextPow = 0 + var/lastI = 0 + var/i + lastTgsError = null + for(i = 1; ; i = base + (2 ** nextPow)) + var/payload = create_payload(i) + + var/list/result = api.Bridge(0, list("chatMessage" = list("text" = "payload:[payload]"))) + + if(!result || lastTgsError || result["integrationHack"] != "ok") + if(i == lastI + 1) + break + lastTgsError = null + i = lastI + base = lastI + nextPow = 0 + continue + + lastI = i + ++nextPow + + var/finalResult = api.Bridge(0, list("chatMessage" = list("text" = "done"))) + if(!finalResult || lastTgsError || finalResult["integrationHack"] != "ok") + text2file("Failed to end bridge limit test!", "test_fail_reason.txt") + del(world) + + api.access_identifier = old_ai diff --git a/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs index ebd46b7bea..862b65ffc8 100644 --- a/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs @@ -12,13 +12,15 @@ namespace Tgstation.Server.Tests.Instance { readonly IInstanceClient instanceClient; readonly IInstanceManagerClient instanceManagerClient; - readonly IInstanceManager instanceManager; + readonly InstanceManager instanceManager; + readonly ushort serverPort; - public InstanceTest(IInstanceClient instanceClient, IInstanceManagerClient instanceManagerClient, IInstanceManager instanceManager) + public InstanceTest(IInstanceClient instanceClient, IInstanceManagerClient instanceManagerClient, InstanceManager instanceManager, ushort serverPort) { this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient)); this.instanceManagerClient = instanceManagerClient ?? throw new ArgumentNullException(nameof(instanceManagerClient)); this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); + this.serverPort = serverPort; } public async Task RunTests(CancellationToken cancellationToken) @@ -38,7 +40,7 @@ namespace Tgstation.Server.Tests.Instance await configTest.Run(cancellationToken); await chatTests; await repoTests; - await new WatchdogTest(instanceClient, instanceManager).Run(cancellationToken); + await new WatchdogTest(instanceClient, instanceManager, serverPort).Run(cancellationToken); } } } diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 4bbb52d663..f744c3d360 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using Newtonsoft.Json; + using System; using System.Globalization; using System.IO; @@ -13,6 +15,7 @@ using System.Net.Sockets; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using System.Web; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; @@ -22,25 +25,29 @@ using Tgstation.Server.Client; using Tgstation.Server.Client.Components; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Watchdog; +using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; namespace Tgstation.Server.Tests.Instance { - sealed class WatchdogTest : JobsRequiredTest + sealed class WatchdogTest : JobsRequiredTest, IBridgeHandler { readonly IInstanceClient instanceClient; - readonly IInstanceManager instanceManager; + readonly InstanceManager instanceManager; + readonly ushort serverPort; bool ranTimeoutTest = false; - public WatchdogTest(IInstanceClient instanceClient, IInstanceManager instanceManager) + public WatchdogTest(IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort) : base(instanceClient.Jobs) { this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient)); this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); + this.serverPort = serverPort; } public async Task Run(CancellationToken cancellationToken) @@ -75,6 +82,7 @@ namespace Tgstation.Server.Tests.Instance await RunLongRunningTestThenUpdate(cancellationToken); await GhettoChatCommandTest(cancellationToken); + await GhettoValidateDMApiLimits(cancellationToken); await RunLongRunningTestThenUpdateWithNewDme(cancellationToken); await RunLongRunningTestThenUpdateWithByondVersionSwitch(cancellationToken); @@ -242,7 +250,7 @@ namespace Tgstation.Server.Tests.Instance using var ddProc = ddProcs.Single(); IProcessExecutor executor = null; executor = new ProcessExecutor( - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new WindowsProcessFeatures(Mock.Of>()) : new PosixProcessFeatures(new Lazy(() => executor), Mock.Of(), Mock.Of>()), Mock.Of(), @@ -328,6 +336,198 @@ namespace Tgstation.Server.Tests.Instance return await instanceClient.DreamDaemon.Start(cancellationToken); } + class DMApiParametersImpl : DMApiParameters { } + public DMApiParameters DMApiParameters => new DMApiParametersImpl + { + AccessIdentifier = "tgs_integration_test" + }; + + TaskCompletionSource bridgeTestsTcs; + + public class BridgeResponseHack : BridgeResponse + { + public string IntegrationHack { get; set; } + } + + public class ResponseTestData : TestData + { + public bool Continue { get; set; } + public string PayloadId { get; set; } + } + + bool bridgeStageResponse; + long lastBridgeRequestSize = 0; + + uint lastPayloadId = 0; + + public Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) + { + try + { + Assert.AreEqual(DMApiParameters.AccessIdentifier, parameters.AccessIdentifier); + Assert.AreEqual((BridgeCommandType)0, parameters.CommandType); + Assert.IsNotNull(parameters.ChatMessage?.Text); + var splits = parameters.ChatMessage.Text.Split(':', StringSplitOptions.RemoveEmptyEntries); + var coreMessage = splits[0]; + Assert.IsFalse(String.IsNullOrWhiteSpace(coreMessage)); + if (!bridgeStageResponse) + if (coreMessage == "done") + { + Assert.AreEqual(DMApiConstants.MaximumBridgeRequestLength, lastBridgeRequestSize); + + bridgeTestsTcs.SetResult(); + return Task.FromResult( + new BridgeResponseHack + { + IntegrationHack = "ok" + }); + } + + Assert.AreEqual("payload", coreMessage); + lastBridgeRequestSize = $"http://127.0.0.1:{serverPort}/Bridge?data=".Length + HttpUtility.UrlEncode( + JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings)).Length; + return Task.FromResult( + new BridgeResponseHack + { + IntegrationHack = "ok" + }); + } + catch (Exception ex) + { + bridgeTestsTcs.SetException(ex); + return Task.FromResult(null); + } + } + + public class TestData + { + public string Size { get; set; } + public string Payload { get; set; } + } + + async Task GhettoValidateDMApiLimits(CancellationToken cancellationToken) + { + var startJob = await StartDD(cancellationToken); + + await WaitForJob(startJob, 40, false, null, cancellationToken); + + // first check the bridge limits + bridgeTestsTcs = new TaskCompletionSource(); + BridgeController.LogContent = false; + using (var bridgeRegistration = instanceManager.RegisterHandler(this)) + { + System.Console.WriteLine("TEST: Sending Bridge tests topic..."); + var bridgeTestTopicResult = await topicClient.SendTopic(IPAddress.Loopback, "tgs_integration_test_tactics2=1", IntegrationTest.DDPort, cancellationToken); + Assert.AreEqual("ack2", bridgeTestTopicResult.StringData); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + // this test doesn't take long + cts.CancelAfter(TimeSpan.FromMinutes(10)); + await bridgeTestsTcs.Task.WithToken(cts.Token); + } + + BridgeController.LogContent = true; + + // Time for DD to revert the bridge access identifier change + await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); + + // Time for topic tests + // Request + + System.Console.WriteLine("TEST: Sending Topic tests topics..."); + + var nextPow = 0; + var lastSize = 0; + + var baseTopic = new TestData + { + Size = 0.ToString().PadLeft(6, '0'), + Payload = "", + }; + + var json = JsonConvert.SerializeObject(baseTopic, DMApiConstants.SerializerSettings); + var topicString = $"tgs_integration_test_tactics3={topicClient.SanitizeString(json)}"; + + var baseSize = topicString.Length; + var wrappingSize = baseSize; + + while (!cancellationToken.IsCancellationRequested) + { + var currentSize = baseSize + (int)Math.Pow(2, nextPow); + var payloadSize = currentSize - wrappingSize; + + var topic = new TestData + { + Size = payloadSize.ToString().PadLeft(6, '0'), + Payload = new string('a', payloadSize), + }; + + TopicResponse topicRequestResult = null; + try + { + topicRequestResult = await topicClient.SendTopic( + IPAddress.Loopback, + $"tgs_integration_test_tactics3={topicClient.SanitizeString(JsonConvert.SerializeObject(topic, DMApiConstants.SerializerSettings))}", + IntegrationTest.DDPort, + cancellationToken); + } + catch (ArgumentOutOfRangeException) + { + } + + if (topicRequestResult == null + || topicRequestResult.ResponseType != TopicResponseType.StringResponse + || topicRequestResult.StringData != "pass") + { + if (topicRequestResult != null) + Assert.AreEqual("fail", topicRequestResult.StringData); + if (currentSize == lastSize + 1) + break; + baseSize = lastSize; + nextPow = 0; + continue; + } + + lastSize = currentSize; + ++nextPow; + } + + cancellationToken.ThrowIfCancellationRequested(); + + Assert.AreEqual(DMApiConstants.MaximumTopicRequestLength, (uint)lastSize); + + // Receive + baseSize = 1; + nextPow = 0; + lastSize = 0; + while (!cancellationToken.IsCancellationRequested) + { + var currentSize = baseSize + (int)Math.Pow(2, nextPow); + var topicRequestResult = await topicClient.SendTopic( + IPAddress.Loopback, + $"tgs_integration_test_tactics4={topicClient.SanitizeString(currentSize.ToString())}", + IntegrationTest.DDPort, + cancellationToken); + + if (topicRequestResult.ResponseType != TopicResponseType.StringResponse + || new string('a', currentSize) != topicRequestResult.StringData) + { + if (currentSize == lastSize + 1) + break; + baseSize = lastSize; + nextPow = 0; + continue; + } + + lastSize = currentSize; + ++nextPow; + } + + cancellationToken.ThrowIfCancellationRequested(); + Assert.AreEqual(DMApiConstants.MaximumTopicResponseLength, (uint)lastSize); + await instanceClient.DreamDaemon.Shutdown(cancellationToken); + } + async Task GhettoChatCommandTest(CancellationToken cancellationToken) { var startJob = await StartDD(cancellationToken); @@ -581,23 +781,23 @@ namespace Tgstation.Server.Tests.Instance return ddProc != null; } + TopicClient topicClient = new (new SocketParameters + { + SendTimeout = TimeSpan.FromSeconds(30), + ReceiveTimeout = TimeSpan.FromSeconds(30), + ConnectTimeout = TimeSpan.FromSeconds(30), + DisconnectTimeout = TimeSpan.FromSeconds(30) + }); + public async Task TellWorldToReboot(CancellationToken cancellationToken) { var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); var initialCompileJob = daemonStatus.ActiveCompileJob; - var bts = new TopicClient(new SocketParameters - { - SendTimeout = TimeSpan.FromSeconds(30), - ReceiveTimeout = TimeSpan.FromSeconds(30), - ConnectTimeout = TimeSpan.FromSeconds(30), - DisconnectTimeout = TimeSpan.FromSeconds(30) - }); - try { System.Console.WriteLine("TEST: Sending world reboot topic..."); - var result = await bts.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", IntegrationTest.DDPort, cancellationToken); + var result = await topicClient.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", IntegrationTest.DDPort, cancellationToken); Assert.AreEqual("ack", result.StringData); using (var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index f14acea7be..282b4a1482 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -829,7 +829,7 @@ namespace Tgstation.Server.Tests TerminateAllDDs(); - IInstanceManager GetInstanceManager() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService(); + InstanceManager GetInstanceManager() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService(); // main run var serverTask = server.Run(cancellationToken); @@ -878,7 +878,7 @@ namespace Tgstation.Server.Tests Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path)); - var instanceTests = FailFast(new InstanceTest(instanceClient, adminClient.Instances, GetInstanceManager()).RunTests(cancellationToken)); + var instanceTests = FailFast(new InstanceTest(instanceClient, adminClient.Instances, GetInstanceManager(), (ushort)server.Url.Port).RunTests(cancellationToken)); await Task.WhenAll(rootTest, adminTest, instanceTests, usersTest); @@ -998,7 +998,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken); - var wdt = new WatchdogTest(instanceClient, GetInstanceManager()); + var wdt = new WatchdogTest(instanceClient, GetInstanceManager(), (ushort)server.Url.Port); await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken); dd = await instanceClient.DreamDaemon.Read(cancellationToken); @@ -1045,7 +1045,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual(WatchdogStatus.Online, currentDD.Status); Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value); - var wdt = new WatchdogTest(instanceClient, GetInstanceManager()); + var wdt = new WatchdogTest(instanceClient, GetInstanceManager(), (ushort)server.Url.Port); currentDD = await wdt.TellWorldToReboot(cancellationToken); Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value); Assert.IsNull(currentDD.StagedCompileJob);