From 944a258aebf337420c2aaf0332228682b2dad956 Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Fri, 19 Jun 2026 15:14:40 +0300 Subject: [PATCH 1/2] switch-to-configuration-ng: handle transition to socket activation When a service gains a `.socket` unit, the socket is not yet active, so the `active_cur.contains_key(socket)` check is false and the service is queued in `units_to_start`. That races with the socket unit being started by `sockets.target`, producing "Socket service already active, refusing" or "no socket activation file descriptors found" errors. Treat the service as socket-activated whenever the socket is absent now but present in the new configuration, leaving it to be activated on demand by the newly started socket rather than started directly. Fixes: https://github.com/NixOS/nixpkgs/pull/510391#issuecomment-4273511176 Assisted-by: Claude:claude-opus-4-8 --- nixos/tests/switch-test.nix | 18 +++++++++++++++++ .../sw/switch-to-configuration-ng/src/main.rs | 20 +++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix index 63dc78e2e6dc..b81d84086f2e 100644 --- a/nixos/tests/switch-test.nix +++ b/nixos/tests/switch-test.nix @@ -541,6 +541,12 @@ in systemd.services."accept-socket@".serviceConfig.X-Test = "test"; }; + socket-activated-without-socket.configuration = { + imports = [ simple-socket.configuration ]; + systemd.sockets.socket-activated.enable = false; + systemd.services.socket-activated.wantedBy = [ "multi-user.target" ]; + }; + mount.configuration = { systemd.mounts = [ { @@ -1620,6 +1626,18 @@ in if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello": raise Exception("Socket was not properly activated after the service was restarted") + # A service transitioning to socket activation is not started directly, + # it's left for the newly started socket to activate on demand + switch_to_specialisation("${machine}", "socket-activated-without-socket") + machine.succeed("systemctl is-active socket-activated.service") + out = switch_to_specialisation("${machine}", "simple-socket-stop-if-changed") + assert_contains(out, "stopping the following units: socket-activated.service\n") + assert_lacks(out, "\nstarting the following units:") + assert_contains(out, "the following new units were started: socket-activated.socket\n") + machine.succeed("[ -S /run/test.sock ]") + if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello": + raise Exception("Socket was not properly activated after the transition") + 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 ]") diff --git a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs index ec6bf1ad253f..b5d307d7bdb1 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs @@ -777,6 +777,9 @@ fn handle_modified_unit( } for socket in &sockets { + let socket_in_new_config = + toplevel.join(scope.etc_dir()).join(socket).exists(); + if active_cur.contains_key(socket) { // We can now be sure this is a socket-activated unit @@ -787,7 +790,7 @@ fn handle_modified_unit( } // Only restart sockets that actually exist in new configuration: - if toplevel.join(scope.etc_dir()).join(socket).exists() { + if socket_in_new_config { if use_restart_as_stop_and_start { units_to_restart.insert(socket.to_string(), ()); record_unit(&restart_list, socket); @@ -798,12 +801,17 @@ fn handle_modified_unit( socket_activated = true; } + } else if socket_in_new_config { + // Transitioning to socket activation; let the socket start it. + socket_activated = true; + } else { + continue; + } - // Remove from units to reload so we don't restart and reload - if units_to_reload.contains_key(unit) { - units_to_reload.remove(unit); - unrecord_unit(&reload_list, unit); - } + // Remove from units to reload so we don't restart and reload + if units_to_reload.contains_key(unit) { + units_to_reload.remove(unit); + unrecord_unit(&reload_list, unit); } } } From cac02061f84ad05a4d8db7c6f245fbe21b70f4c2 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Tue, 23 Jun 2026 14:21:17 +0200 Subject: [PATCH 2/2] switch-to-configuration-ng: pull common code out of the different conditional branches All branches here are restarting instead of reloading, so it makes sense to remove the unit from units_to_reload in all cases. --- .../sw/switch-to-configuration-ng/src/main.rs | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs index b5d307d7bdb1..55d1d93bde34 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs @@ -743,11 +743,6 @@ fn handle_modified_unit( // This unit should be restarted instead of stopped and started. units_to_restart.insert(unit.to_string(), ()); record_unit(&restart_list, unit); - // Remove from units to reload so we don't restart and reload - if units_to_reload.contains_key(unit) { - units_to_reload.remove(unit); - unrecord_unit(&reload_list, unit); - } } else { // If this unit is socket-activated, then stop the socket unit(s) as well, and // restart the socket(s) instead of the service. @@ -804,14 +799,6 @@ fn handle_modified_unit( } else if socket_in_new_config { // Transitioning to socket activation; let the socket start it. socket_activated = true; - } else { - continue; - } - - // Remove from units to reload so we don't restart and reload - if units_to_reload.contains_key(unit) { - units_to_reload.remove(unit); - unrecord_unit(&reload_list, unit); } } } @@ -840,11 +827,11 @@ fn handle_modified_unit( } else { units_to_stop.insert(unit.to_string(), ()); } - // Remove from units to reload so we don't restart and reload - if units_to_reload.contains_key(unit) { - units_to_reload.remove(unit); - unrecord_unit(&reload_list, unit); - } + } + + // Remove from units to reload so we don't restart and reload + if units_to_reload.remove(unit).is_some() { + unrecord_unit(&reload_list, unit); } } }