From 177cc5bd7fb6a5d3e6ae8be415635d0732a28222 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 11 Jul 2026 06:07:41 +0200 Subject: [PATCH] Systemd notify hooks, move nixos integration tests to a systemd service --- .../Spacebar.Tests/Tests/MessageTests.cs | 2 +- .../Spacebar.Tests/Tests/WebhookTests.cs | 6 +- nix/modules/default/default.nix | 3 + nix/modules/default/pion-sfu.nix | 1 + nix/tests/test-bundle-starts.nix | 100 ++++++++++++------ package-lock.json | Bin 297031 -> 302435 bytes package.json | 1 + src/api/Server.ts | 5 +- src/cdn/Server.ts | 5 +- src/gateway/Server.ts | 3 +- src/util/util/ProcessLifecycle.ts | 35 ++++++ src/webrtc/Server.ts | 3 +- 12 files changed, 124 insertions(+), 40 deletions(-) diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs index d563b244a..e87d7f8fc 100644 --- a/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs @@ -181,7 +181,7 @@ public class MessageTests(ITestOutputHelper testOutputHelper, TestFixture fixtur } public static IEnumerable WebhookExecuteCombinations() { - string[] contents = ["meow", "# hi!!!", "https://spacebar.chat/favicon.ico", "@everyone", "@here"]; + string[] contents = ["meow", "# hi!!!", Client.ClientWellKnown.Api.BaseUrl, "@everyone", "@here"]; bool?[] ttsEnabled = [null, true, false]; int?[] messageFlags = [ null, diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/WebhookTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/WebhookTests.cs index 682192169..965f2f05f 100644 --- a/extra/admin-api/Tests/Spacebar.Tests/Tests/WebhookTests.cs +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/WebhookTests.cs @@ -119,9 +119,9 @@ public class WebhookTests(ITestOutputHelper testOutputHelper, TestFixture fixtur } public static IEnumerable WebhookExecuteCombinations() { - string[] contents = ["meow", "# hi!!!", "https://spacebar.chat/favicon.ico", "@everyone", "@here"]; + string[] contents = ["meow", "# hi!!!", Client.ClientWellKnown.Api.BaseUrl, "@everyone", "@here"]; string?[] usernames = [null, "meow"]; - string?[] avatarUrls = [null, "https://spacebar.chat/favicon.ico"]; + string?[] avatarUrls = [null, Client.ClientWellKnown.Api.BaseUrl + "/static/logo.png"]; bool?[] ttsEnabled = [null, true, false]; int?[] messageFlags = [ null, @@ -157,7 +157,7 @@ public class WebhookTests(ITestOutputHelper testOutputHelper, TestFixture fixtur public async Task SendWebhookMessageWithAvatarUrl() { await Assert.SuccessfullyHttpPostAsJsonAsync(Webhook.Url + "?wait=true", new JsonObject() { { "content", "meow" }, - { "avatar_url", "https://spacebar.chat/favicon.ico" } + { "avatar_url", Client.ClientWellKnown.Api.BaseUrl + "/static/logo.png" } }); } } \ No newline at end of file diff --git a/nix/modules/default/default.nix b/nix/modules/default/default.nix index 6c6437a85..12abfe777 100644 --- a/nix/modules/default/default.nix +++ b/nix/modules/default/default.nix @@ -127,6 +127,7 @@ in ); serviceConfig = { ExecStart = "${cfg.package}/bin/start-api"; + Type = "notify"; }; }; @@ -151,6 +152,7 @@ in ); serviceConfig = { ExecStart = "${cfg.package}/bin/start-gateway"; + Type = "notify"; }; }; @@ -174,6 +176,7 @@ in ); serviceConfig = { ExecStart = "${cfg.package}/bin/start-cdn"; + Type = "notify"; }; }); }; diff --git a/nix/modules/default/pion-sfu.nix b/nix/modules/default/pion-sfu.nix index 92bcf5330..cd8c88cb1 100644 --- a/nix/modules/default/pion-sfu.nix +++ b/nix/modules/default/pion-sfu.nix @@ -69,6 +69,7 @@ in ); serviceConfig = { ExecStart = "${cfg.package}/bin/start-webrtc"; + Type = "notify"; }; }; diff --git a/nix/tests/test-bundle-starts.nix b/nix/tests/test-bundle-starts.nix index 8fd6b98e9..b2be0bb86 100644 --- a/nix/tests/test-bundle-starts.nix +++ b/nix/tests/test-bundle-starts.nix @@ -12,6 +12,16 @@ let sb = import ../lib/mkEndpoint.nix; isRabbitMqTest = lib.strings.hasPrefix "rabbitmq" withIpc; + + testBin = lib.getExe self.outputs.packages.${pkgs.stdenv.system}.Spacebar-Tests; + testConfigPath = pkgs.writeText "Spacebar-Tests-appsettings.json" ( + builtins.toJSON { + Configuration = { + TestInstance = "http://localhost:3001"; + RegisterConcurrentCount = 150; + }; + } + ); in { name = "test-bundle-starts" + lib.optionalString (withIpc != "unix") ("_ipc=" + withIpc); @@ -60,6 +70,45 @@ in lib.trace ("Testing with config: " + builtins.toJSON cfg) cfg; services.nginx.enable = true; services.rabbitmq.enable = isRabbitMqTest; + + # ...fix startup ordering + systemd.services = + let + services = [ "postgresql.service" ] ++ lib.optional (isRabbitMqTest) [ "rabbitmq.service" ]; + serviceDef = { + after = services; + wants = services; + }; + in + { + "spacebar-api" = serviceDef; + "spacebar-cdn" = serviceDef; + "spacebar-gateway" = serviceDef; + "spacebar-webrtc" = serviceDef; + + "spacebar-tests" = { + documentation = [ "https://docs.spacebar.chat/" ]; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ + "network-online.target" + "spacebar-api.service" + ]; + requires = [ "spacebar-api.service" ]; + environment = { + TEST_APPSETTINGS_PATH=testConfigPath; + }; + serviceConfig = { + ExecStart = "${testBin} -reporter verbose -parallelAlgorithm aggressive -maxThreads unlimited -preEnumerateTheories"; + DynamicUser = true; + RestrictSUIDSGID = true; + + Restart = "no"; + UMask = "077"; + }; + }; + }; + services.postgresql = { enable = true; initdbArgs = [ @@ -86,37 +135,28 @@ in # https://nixos.org/manual/nixos/stable/index.html#sec-nixos-tests # https://nixos.org/manual/nixpkgs/unstable/#tester-runNixOSTest - testScript = - let - testBin = lib.getExe self.outputs.packages.${pkgs.stdenv.system}.Spacebar-Tests; - testConfigPath = pkgs.writeText "Spacebar-Tests-appsettings.json" ( - builtins.toJSON { - Configuration = { - TestInstance = "http://localhost:3001"; - RegisterConcurrentCount = 150; - }; - } - ); - in - '' - machine.wait_for_unit("spacebar-api") - machine.wait_for_unit("spacebar-cdn") - machine.wait_for_unit("spacebar-gateway") - # Wait for unit doesn't mean the service is actually ready to accept connections - machine.wait_for_open_port(80) - machine.wait_for_open_port(3001) - machine.wait_for_open_port(3002) - machine.wait_for_open_port(3003) + testScript = '' + machine.wait_for_unit("spacebar-api") + machine.wait_for_unit("spacebar-cdn") + machine.wait_for_unit("spacebar-gateway") + # Wait for unit doesn't mean the service is actually ready to accept connections + machine.wait_for_open_port(80) + machine.wait_for_open_port(3001) + machine.wait_for_open_port(3002) + machine.wait_for_open_port(3003) - # this should be working - machine.succeed("curl -f http://api.sb.localhost/.well-known/spacebar/client") + # this should be working + machine.succeed("curl -f http://api.sb.localhost/.well-known/spacebar/client") - # check if metrics endpoint works on all services - machine.succeed("curl -f http://api.sb.localhost/metrics") - machine.succeed("curl -f http://gateway.sb.localhost/metrics") - machine.succeed("curl -f http://cdn.sb.localhost/metrics") + # check if metrics endpoint works on all services + machine.succeed("curl -f http://api.sb.localhost/metrics") + machine.succeed("curl -f http://gateway.sb.localhost/metrics") + machine.succeed("curl -f http://cdn.sb.localhost/metrics") - # run integration tests - machine.succeed("/usr/bin/env TEST_APPSETTINGS_PATH=${testConfigPath} ${testBin} -reporter verbose -parallelAlgorithm aggressive -maxThreads unlimited -preEnumerateTheories") - ''; + machine.wait_for_unit("spacebar-tests") + machine.wait_until_fails("systemctl show spacebar-tests.service | grep 'SubState=running' -q") + # run integration tests + # machine.succeed("/usr/bin/env TEST_APPSETTINGS_PATH=${testConfigPath} ${testBin} -reporter verbose -parallelAlgorithm aggressive -maxThreads unlimited -preEnumerateTheories") + + ''; } diff --git a/package-lock.json b/package-lock.json index 64c4ebc7ce1f3184d4e0be23170f88c6ff384f57..627f267fb64dcf5aa47641780677423fe1257bfc 100644 GIT binary patch delta 2010 zcmai!&2QUu7{`?+tB}fRFtMtmO}k1k+{~%tI8K==>?Dot#BpBjBoab?iDM^z$(!TE zClH4nX=j8*#erRSK$AFB8c5ZEiN66Map4T?58yyUTanPDJNWeb=+X0ezR%;I4{!ed z?B>(Ym)*lu;PsGYsH0Hd6x)q~_xF*1-P`EJNuSSj+gykZm&#nUiXPF z=&h~<_PpRd|Bc{O1uz5}rqq~%2cfX{@tS|9oOUD?v_&VJl)_}b?Pw??m(n_;M=Y9V zhKvS{Id+_iq@s$XMFu1g)+(%B6XQ_DsB?p8k5hW>ZWcEy>d?uuYA0bb-qD@F#uwi% z?fHU_xS;p)p8qxPo0flTV)ph-6B6IERU^drbOheo3vPM7NB-3bwl)tN{SVrnfi%^y zMw3#ZreQUBK~?0LlEL{j0#EoEa&sZ6TYYT?U7v%VwRxrF)&lObVE8`dVlN}!>0SSg zm8s<1KTqCs4<84-wUq$+Z?1b&uc7Jr%?o+@{UY%SVwdm;ipU2Bhb1|-gK5bw8q?|| z7;ZV)6c)q#MG-03I&BlCQ3vZnqh+VaSX?b;45E90BeoJL*Cm2bGZrlNX<}E5xSwpl z;f7Za-Op~_@}9_IuZmD817B_4xS4kbGnC7Z)6 zkd$Tb@z?%0&tz8JkJhZYLL1X9&)GZA=L>B97a8#ie4gr@s+R%cVgeeZWq_9Zak#B8 zNS@0;JSiTBq${!ih3QT$W7l zx#XK{=f3;bi`%aD;}5thr{j60PC-5Sd>f#uXA_Rr5 zH8V=8i8wh$O~#BQNe#jdrEum!Odxv{U&NX?*Oy8eFqJ{2FpnJ6Ivh!}Y$Ki!`&dT+ zG;ed?$GCsI_;pe5zdZ+Ldw+1X*NZ>X;yf;ZOA`!_=unO9R;t;OAv7GW$Mca`Ay?vA zHIIX+U9;G}s#Z_* zeN`I6pLS-A_U9W-W`DiXbUF^pSF%MU+ODceCC+9`u$)JQ8qliqGCm$^ExIR-)07BC zo$3GtbA?Pa*G+Xb(2R(%ly74IE0Z-iO&F~t#WCWjfC2Wj(=X= { whyIsNodeRunning(); console.log("\nProcess state:", ProcessLifecycle.state); }); + +export class SystemdLifecycle { + private static writeData(data: string): Promise { + const socketPath = process.env.NOTIFY_SOCKET; + if (!socketPath) return Promise.resolve(); + + const buf = Buffer.from(data); + console.log("Systemd notify socket path:", socketPath, "-", buf.length, "bytes"); + + return new Promise((res, rej) => { + new DgramSocket().sendTo(buf, 0, buf.length, socketPath, (err) => { + if (err) rej(err); + res(); + }); + }); + } + static async sendReady() { + await this.writeData("READY=1"); + } + + static async sendStopping() { + await this.writeData("STOPPING=1"); + } + + static async setStatus(status: string) { + await this.writeData("STATUS=" + status); + } + + // TODO: do we want to support the watchdog? +} diff --git a/src/webrtc/Server.ts b/src/webrtc/Server.ts index d86d8eaa9..a46d1293d 100644 --- a/src/webrtc/Server.ts +++ b/src/webrtc/Server.ts @@ -21,7 +21,7 @@ import ws from "ws"; import { green, yellow } from "picocolors"; import { initDatabase } from "@spacebar/database"; import { Config, initEvent, JwtKeypairManager } from "@spacebar/util"; -import { ProcessLifecycle } from "../util/util/ProcessLifecycle"; +import { ProcessLifecycle, SystemdLifecycle } from "../util/util/ProcessLifecycle"; import { Monitoring } from "../util/monitoring/Monitoring"; import { Connection } from "./events/Connection"; import { loadWebRtcLibrary, mediaServer, WRTC_PORT_MAX, WRTC_PORT_MIN, WRTC_PUBLIC_IP } from "./util"; @@ -82,6 +82,7 @@ export class Server { if (!this.server.listening) { this.server.listen(this.port); console.log(`[WebRTC] ${green(`online on 0.0.0.0:${this.port}`)}`); + await SystemdLifecycle.setStatus(`Listening on 0.0.0.0:${this.port}...`); } await ProcessLifecycle.Ready();