From 9a0684874c75bab52ff00311ee95713892c9d94f Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Fri, 19 Jun 2026 12:36:40 +0300 Subject: [PATCH] switch-to-configuration-ng: handle socket-activated Accept=yes services A per-connection instance spawned by an Accept=yes socket (e.g. foo@1234.service) has an implicit socket of foo.socket, but the implicit name was derived from the template base name including the trailing "@", yielding foo@.socket. That never matches an active unit, so such running instances were treated as non-socket-activated and the activation script tried to start them directly on a configuration switch. That fails because there is no connection socket to pass, sending the instance into a restart loop. Strip the trailing "@" so the implicit socket resolves correctly; the running instances are then stopped and their socket restarted, leaving systemd to spawn fresh instances for new connections. Resolves: https://github.com/NixOS/nixpkgs/issues/533205 Assisted-by: Claude:claude-opus-4-8 (cherry picked from commit ad3e95b0931c8c618dc3fd017174fd97302c5538) --- nixos/tests/switch-test.nix | 64 +++++++++++++++++++ .../sw/switch-to-configuration-ng/src/main.rs | 6 +- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix index 2dbc9386f8ae..63dc78e2e6dc 100644 --- a/nixos/tests/switch-test.nix +++ b/nixos/tests/switch-test.nix @@ -43,6 +43,19 @@ let server.serve_forever() ''; + # Per-connection (Accept=yes) socket-activated service that requires the + # connection socket to be passed via socket activation and fails when started + # without one. It greets the client and stays alive for as long as the + # connection is held open. + acceptSocketTest = pkgs.writeShellScript "accept-socket-test.sh" '' + if [ "''${LISTEN_FDS:-0}" -lt 1 ]; then + echo "Expected exactly one socket, got 0" >&2 + exit 4 + fi + printf hello >&3 + exec ${lib.getExe' pkgs.coreutils "cat"} <&3 >/dev/null + ''; + in { name = "switch-test"; @@ -508,6 +521,26 @@ in }; }; + accept-socket.configuration = { + systemd.sockets.accept-socket = { + wantedBy = [ "sockets.target" ]; + listenStreams = [ "/run/accept-test.sock" ]; + socketConfig = { + Accept = "yes"; + SocketMode = "0777"; + }; + }; + systemd.services."accept-socket@" = { + description = "A per-connection socket-activated service"; + serviceConfig.ExecStart = acceptSocketTest; + }; + }; + + accept-socket-service-modified.configuration = { + imports = [ accept-socket.configuration ]; + systemd.services."accept-socket@".serviceConfig.X-Test = "test"; + }; + mount.configuration = { systemd.mounts = [ { @@ -1587,6 +1620,37 @@ in if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello": raise Exception("Socket was not properly activated after the service was restarted") + with subtest("socket-activated services with Accept=yes"): + # Socket-activated services don't get started, just the socket + machine.fail("[ -S /run/accept-test.sock ]") + out = switch_to_specialisation("${machine}", "accept-socket") + assert_contains(out, "the following new units were started: accept-socket.socket\n") + machine.succeed("[ -S /run/accept-test.sock ]") + + # Hold a connection open so a per-connection instance keeps running + machine.succeed("socat EXEC:'sleep infinity' UNIX-CONNECT:/run/accept-test.sock >&2 &") + instance = machine.wait_until_succeeds( + "systemctl list-units --no-legend --state=running 'accept-socket@*.service' " + + "| grep -m1 -o 'accept-socket@[^ ]*\\.service'" + ).strip() + + # Changing the templated service must stop the running instance and + # restart the socket instead of (re)starting the per-connection + # instance, which cannot be started without a connection socket + out = switch_to_specialisation("${machine}", "accept-socket-service-modified") + assert_contains(out, "stopping the following units:") + assert_contains(out, instance) + assert_contains(out, "accept-socket.socket") + assert_contains(out, "\nstarting the following units: accept-socket.socket\n") + assert_lacks(out, "NOT restarting the following changed units:") + assert_lacks(out, "\nrestarting the following units:") + # The per-connection instance must not be (re)started + starting = out[out.index("\nstarting the following units:") :] + assert instance not in starting, f"instance {instance} should not be (re)started" + # Socket-activation of the unit still works + if machine.succeed("socat - UNIX-CONNECT:/run/accept-test.sock