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 ad3e95b093)
This commit is contained in:
Ilan Joselevich 2026-06-19 12:36:40 +03:00 committed by github-actions[bot]
commit 9a0684874c
2 changed files with 69 additions and 1 deletions

View file

@ -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 </dev/null") != "hello":
raise Exception("Socket was not properly activated after the service was changed")
with subtest("mounts"):
switch_to_specialisation("${machine}", "mount")
out = machine.succeed("mount | grep 'on /testmount'")

View file

@ -769,7 +769,11 @@ fn handle_modified_unit(
};
if sockets.is_empty() {
sockets.push(format!("{base_name}.socket"));
// For a templated instance (`foo@bar.service`), `base_name`
// includes the trailing `@`; the implicitly-associated socket
// is `foo.socket`, so strip it.
let socket_base = base_name.strip_suffix('@').unwrap_or(base_name);
sockets.push(format!("{socket_base}.socket"));
}
for socket in &sockets {