diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml index 023f0f57a9b2..d3046b88bfec 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml @@ -1554,6 +1554,15 @@ Superuser created successfully. encapsulation. + + + Changing systemd .socket units now restarts + them and stops the service that is activated by them. + Additionally, services with + stopOnChange = false don’t break anymore + when they are socket-activated. + + diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md index 658f3b59da67..e84ca7ed8cc9 100644 --- a/nixos/doc/manual/release-notes/rl-2111.section.md +++ b/nixos/doc/manual/release-notes/rl-2111.section.md @@ -449,3 +449,5 @@ In addition to numerous new and upgraded packages, this release has the followin - The `networking` module has a new `networking.fooOverUDP` option to configure Foo-over-UDP encapsulations. - `networking.sits` now supports Foo-over-UDP encapsulation. + +- Changing systemd `.socket` units now restarts them and stops the service that is activated by them. Additionally, services with `stopOnChange = false` don't break anymore when they are socket-activated. diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl index 027e050511fb..ced49773d1ed 100644 --- a/nixos/modules/system/activation/switch-to-configuration.pl +++ b/nixos/modules/system/activation/switch-to-configuration.pl @@ -19,11 +19,14 @@ my $startListFile = "/run/nixos/start-list"; my $restartListFile = "/run/nixos/restart-list"; my $reloadListFile = "/run/nixos/reload-list"; -# Parse restart/reload requests by the activation script +# Parse restart/reload requests by the activation script. +# Activation scripts may write newline-separated units to this +# file and switch-to-configuration will handle them. While +# `stopIfChanged = true` is ignored, switch-to-configuration will +# handle `restartIfChanged = false` and `reloadIfChanged = true`. +# This also works for socket-activated units. my $restartByActivationFile = "/run/nixos/activation-restart-list"; -my $reloadByActivationFile = "/run/nixos/activation-reload-list"; my $dryRestartByActivationFile = "/run/nixos/dry-activation-restart-list"; -my $dryReloadByActivationFile = "/run/nixos/dry-activation-reload-list"; make_path("/run/nixos", { mode => oct(755) }); @@ -147,6 +150,87 @@ sub fingerprintUnit { return abs_path($s) . (-f "${s}.d/overrides.conf" ? " " . abs_path "${s}.d/overrides.conf" : ""); } +sub handleModifiedUnit { + my ($unit, $baseName, $newUnitFile, $activePrev, $unitsToStop, $unitsToStart, $unitsToReload, $unitsToRestart, $unitsToSkip) = @_; + + if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target") { + # Do nothing. These cannot be restarted directly. + # Slices and Paths don't have to be restarted since + # properties (resource limits and inotify watches) + # seem to get applied on daemon-reload. + } elsif ($unit =~ /\.mount$/) { + # Reload the changed mount unit to force a remount. + $unitsToReload->{$unit} = 1; + recordUnit($reloadListFile, $unit); + } elsif ($unit =~ /\.slice$/ || $unit =~ /\.path$/) { + # FIXME: do something? + } else { + my $unitInfo = parseUnit($newUnitFile); + if (boolIsTrue($unitInfo->{'X-ReloadIfChanged'} // "no")) { + $unitsToReload->{$unit} = 1; + recordUnit($reloadListFile, $unit); + } + elsif (!boolIsTrue($unitInfo->{'X-RestartIfChanged'} // "yes") || boolIsTrue($unitInfo->{'RefuseManualStop'} // "no") || boolIsTrue($unitInfo->{'X-OnlyManualStart'} // "no")) { + $unitsToSkip->{$unit} = 1; + } else { + # If this unit is socket-activated, then stop it instead + # of restarting it to make sure the new version of it is + # socket-activated. + my $socketActivated = 0; + if ($unit =~ /\.service$/) { + my @sockets = split / /, ($unitInfo->{Sockets} // ""); + if (scalar @sockets == 0) { + @sockets = ("$baseName.socket"); + } + foreach my $socket (@sockets) { + if (defined $activePrev->{$socket}) { + # Only restart sockets that actually + # exist in new configuration + if (-e "$out/etc/systemd/system/$socket") { + $socketActivated = 1; + $unitsToStop->{$unit} = 1; + } + } + } + } + # Don't do the rest of this for socket-activated units + # because we handled these above where we stop the unit. + # Since only services can be socket-activated, the + # following condition always evaluates to `true` for + # non-service units. + if (!$socketActivated) { + # If we are restarting a socket, also stop the corresponding + # service. This is required because restarting a socket + # when the service is already activated fails. + if ($unit =~ /\.socket$/) { + my $service = $unitInfo->{Service} // ""; + if ($service eq "") { + $service = "$baseName.service"; + } + if (defined $activePrev->{$service}) { + $unitsToStop->{$service} = 1; + } + $unitsToRestart->{$unit} = 1; + recordUnit($restartListFile, $unit); + } else { + if (!boolIsTrue($unitInfo->{'X-StopIfChanged'} // "yes")) { + # This unit should be restarted instead of + # stopped and started. + $unitsToRestart->{$unit} = 1; + recordUnit($restartListFile, $unit); + } else { + # We write to a file to ensure that the + # service gets restarted if we're interrupted. + $unitsToStart->{$unit} = 1; + recordUnit($startListFile, $unit); + $unitsToStop->{$unit} = 1; + } + } + } + } + } +} + # Figure out what units need to be stopped, started, restarted or reloaded. my (%unitsToStop, %unitsToSkip, %unitsToStart, %unitsToRestart, %unitsToReload); @@ -219,65 +303,7 @@ while (my ($unit, $state) = each %{$activePrev}) { } elsif (fingerprintUnit($prevUnitFile) ne fingerprintUnit($newUnitFile)) { - if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target") { - # Do nothing. These cannot be restarted directly. - } elsif ($unit =~ /\.mount$/) { - # Reload the changed mount unit to force a remount. - $unitsToReload{$unit} = 1; - recordUnit($reloadListFile, $unit); - } elsif ($unit =~ /\.socket$/ || $unit =~ /\.path$/ || $unit =~ /\.slice$/) { - # FIXME: do something? - } else { - my $unitInfo = parseUnit($newUnitFile); - if (boolIsTrue($unitInfo->{'X-ReloadIfChanged'} // "no")) { - $unitsToReload{$unit} = 1; - recordUnit($reloadListFile, $unit); - } - elsif (!boolIsTrue($unitInfo->{'X-RestartIfChanged'} // "yes") || boolIsTrue($unitInfo->{'RefuseManualStop'} // "no") || boolIsTrue($unitInfo->{'X-OnlyManualStart'} // "no")) { - $unitsToSkip{$unit} = 1; - } else { - if (!boolIsTrue($unitInfo->{'X-StopIfChanged'} // "yes")) { - # This unit should be restarted instead of - # stopped and started. - $unitsToRestart{$unit} = 1; - recordUnit($restartListFile, $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. - my $socketActivated = 0; - if ($unit =~ /\.service$/) { - my @sockets = split / /, ($unitInfo->{Sockets} // ""); - if (scalar @sockets == 0) { - @sockets = ("$baseName.socket"); - } - foreach my $socket (@sockets) { - if (defined $activePrev->{$socket}) { - $unitsToStop{$socket} = 1; - # Only restart sockets that actually - # exist in new configuration: - if (-e "$out/etc/systemd/system/$socket") { - $unitsToStart{$socket} = 1; - recordUnit($startListFile, $socket); - $socketActivated = 1; - } - } - } - } - - # If the unit is not socket-activated, record - # that this unit needs to be started below. - # We write this to a file to ensure that the - # service gets restarted if we're interrupted. - if (!$socketActivated) { - $unitsToStart{$unit} = 1; - recordUnit($startListFile, $unit); - } - - $unitsToStop{$unit} = 1; - } - } - } + handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, %unitsToSkip); } } } @@ -362,8 +388,6 @@ sub filterUnits { } my @unitsToStopFiltered = filterUnits(\%unitsToStop); -my @unitsToStartFiltered = filterUnits(\%unitsToStart); - # Show dry-run actions. if ($action eq "dry-activate") { @@ -375,21 +399,45 @@ if ($action eq "dry-activate") { print STDERR "would activate the configuration...\n"; system("$out/dry-activate", "$out"); - $unitsToRestart{$_} = 1 foreach - split('\n', read_file($dryRestartByActivationFile, err_mode => 'quiet') // ""); + # Handle the activation script requesting the restart or reload of a unit. + my %unitsToAlsoStop; + my %unitsToAlsoSkip; + foreach (split('\n', read_file($dryRestartByActivationFile, err_mode => 'quiet') // "")) { + my $unit = $_; + my $baseUnit = $unit; + my $newUnitFile = "$out/etc/systemd/system/$baseUnit"; - $unitsToReload{$_} = 1 foreach - split('\n', read_file($dryReloadByActivationFile, err_mode => 'quiet') // ""); + # Detect template instances. + if (!-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) { + $baseUnit = "$1\@.$2"; + $newUnitFile = "$out/etc/systemd/system/$baseUnit"; + } + + my $baseName = $baseUnit; + $baseName =~ s/\.[a-z]*$//; + + handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToAlsoStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, %unitsToAlsoSkip); + } + unlink($dryRestartByActivationFile); + + my @unitsToAlsoStopFiltered = filterUnits(\%unitsToAlsoStop); + if (scalar(keys %unitsToAlsoStop) > 0) { + print STDERR "would stop the following units as well: ", join(", ", @unitsToAlsoStopFiltered), "\n" + if scalar @unitsToAlsoStopFiltered; + } + +print STDERR "NOT restarting the following changed units as well: ", join(", ", sort(keys %unitsToAlsoSkip)), "\n" + if scalar(keys %unitsToAlsoSkip) > 0; print STDERR "would restart systemd\n" if $restartSystemd; print STDERR "would restart the following units: ", join(", ", sort(keys %unitsToRestart)), "\n" if scalar(keys %unitsToRestart) > 0; + my @unitsToStartFiltered = filterUnits(\%unitsToStart); print STDERR "would start the following units: ", join(", ", @unitsToStartFiltered), "\n" if scalar @unitsToStartFiltered; print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n" if scalar(keys %unitsToReload) > 0; unlink($dryRestartByActivationFile); - unlink($dryReloadByActivationFile); exit 0; } @@ -414,12 +462,38 @@ system("$out/activate", "$out") == 0 or $res = 2; # Handle the activation script requesting the restart or reload of a unit. # We can only restart and reload (not stop/start) because the units to be -# stopped are already stopped before the activation script is run. -$unitsToRestart{$_} = 1 foreach - split('\n', read_file($restartByActivationFile, err_mode => 'quiet') // ""); +# stopped are already stopped before the activation script is run. We do however +# make an exception for services that are socket-activated and that have to be stopped +# instead of being restarted. +my %unitsToAlsoStop; +my %unitsToAlsoSkip; +foreach (split('\n', read_file($restartByActivationFile, err_mode => 'quiet') // "")) { + my $unit = $_; + my $baseUnit = $unit; + my $newUnitFile = "$out/etc/systemd/system/$baseUnit"; -$unitsToReload{$_} = 1 foreach - split('\n', read_file($reloadByActivationFile, err_mode => 'quiet') // ""); + # Detect template instances. + if (!-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) { + $baseUnit = "$1\@.$2"; + $newUnitFile = "$out/etc/systemd/system/$baseUnit"; + } + + my $baseName = $baseUnit; + $baseName =~ s/\.[a-z]*$//; + + handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToAlsoStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, %unitsToAlsoSkip); +} +unlink($restartByActivationFile); + +my @unitsToAlsoStopFiltered = filterUnits(\%unitsToAlsoStop); +if (scalar(keys %unitsToAlsoStop) > 0) { + print STDERR "stopping the following units as well: ", join(", ", @unitsToAlsoStopFiltered), "\n" + if scalar @unitsToAlsoStopFiltered; + system("$curSystemd/systemctl", "stop", "--", sort(keys %unitsToAlsoStop)); +} + +print STDERR "NOT restarting the following changed units as well: ", join(", ", sort(keys %unitsToAlsoSkip)), "\n" + if scalar(keys %unitsToAlsoSkip) > 0; # Restart systemd if necessary. Note that this is done using the # current version of systemd, just in case the new one has trouble @@ -460,14 +534,28 @@ if (scalar(keys %unitsToReload) > 0) { print STDERR "reloading the following units: ", join(", ", sort(keys %unitsToReload)), "\n"; system("@systemd@/bin/systemctl", "reload", "--", sort(keys %unitsToReload)) == 0 or $res = 4; unlink($reloadListFile); - unlink($reloadByActivationFile); } # Restart changed services (those that have to be restarted rather # than stopped and started). if (scalar(keys %unitsToRestart) > 0) { print STDERR "restarting the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"; - system("@systemd@/bin/systemctl", "restart", "--", sort(keys %unitsToRestart)) == 0 or $res = 4; + + # We split the units to be restarted into sockets and non-sockets. + # This is because restarting sockets may fail which is not bad by + # itself but which will prevent changes on the sockets. We usually + # restart the socket and stop the service before that. Restarting + # the socket will fail however when the service was re-activated + # in the meantime. There is no proper way to prevent that from happening. + my @unitsWithErrorHandling = grep { $_ !~ /\.socket$/ } sort(keys %unitsToRestart); + my @unitsWithoutErrorHandling = grep { $_ =~ /\.socket$/ } sort(keys %unitsToRestart); + + if (scalar(@unitsWithErrorHandling) > 0) { + system("@systemd@/bin/systemctl", "restart", "--", @unitsWithErrorHandling) == 0 or $res = 4; + } + if (scalar(@unitsWithoutErrorHandling) > 0) { + system("@systemd@/bin/systemctl", "restart", "--", @unitsWithoutErrorHandling); + } unlink($restartListFile); unlink($restartByActivationFile); } @@ -478,6 +566,7 @@ if (scalar(keys %unitsToRestart) > 0) { # that are symlinks to other units. We shouldn't start both at the # same time because we'll get a "Failed to add path to set" error from # systemd. +my @unitsToStartFiltered = filterUnits(\%unitsToStart); print STDERR "starting the following units: ", join(", ", @unitsToStartFiltered), "\n" if scalar @unitsToStartFiltered; system("@systemd@/bin/systemctl", "start", "--", sort(keys %unitsToStart)) == 0 or $res = 4; diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix index 78adf7ffa7da..76a9ef6f624e 100644 --- a/nixos/tests/switch-test.nix +++ b/nixos/tests/switch-test.nix @@ -7,15 +7,135 @@ import ./make-test-python.nix ({ pkgs, ...} : { }; nodes = { - machine = { ... }: { + machine = { config, pkgs, lib, ... }: { + environment.systemPackages = [ pkgs.socat ]; # for the socket activation stuff users.mutableUsers = false; + + specialisation = { + # A system with a simple socket-activated unit + simple-socket.configuration = { + systemd.services.socket-activated.serviceConfig = { + ExecStart = pkgs.writeScript "socket-test.py" /* python */ '' + #!${pkgs.python3}/bin/python3 + + from socketserver import TCPServer, StreamRequestHandler + import socket + + class Handler(StreamRequestHandler): + def handle(self): + self.wfile.write("hello".encode("utf-8")) + + class Server(TCPServer): + def __init__(self, server_address, handler_cls): + # Invoke base but omit bind/listen steps (performed by systemd activation!) + TCPServer.__init__( + self, server_address, handler_cls, bind_and_activate=False) + # Override socket + self.socket = socket.fromfd(3, self.address_family, self.socket_type) + + if __name__ == "__main__": + server = Server(("localhost", 1234), Handler) + server.serve_forever() + ''; + }; + systemd.sockets.socket-activated = { + wantedBy = [ "sockets.target" ]; + listenStreams = [ "/run/test.sock" ]; + socketConfig.SocketMode = lib.mkDefault "0777"; + }; + }; + + # The same system but the socket is modified + modified-socket.configuration = { + imports = [ config.specialisation.simple-socket.configuration ]; + systemd.sockets.socket-activated.socketConfig.SocketMode = "0666"; + }; + + # The same system but the service is modified + modified-service.configuration = { + imports = [ config.specialisation.simple-socket.configuration ]; + systemd.services.socket-activated.serviceConfig.X-Test = "test"; + }; + + # The same system but both service and socket are modified + modified-service-and-socket.configuration = { + imports = [ config.specialisation.simple-socket.configuration ]; + systemd.services.socket-activated.serviceConfig.X-Test = "some_value"; + systemd.sockets.socket-activated.socketConfig.SocketMode = "0444"; + }; + + # A system with a socket-activated service and some simple services + service-and-socket.configuration = { + imports = [ config.specialisation.simple-socket.configuration ]; + systemd.services.simple-service = { + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.coreutils}/bin/true"; + }; + }; + + systemd.services.simple-restart-service = { + stopIfChanged = false; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.coreutils}/bin/true"; + }; + }; + + systemd.services.simple-reload-service = { + reloadIfChanged = true; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.coreutils}/bin/true"; + ExecReload = "${pkgs.coreutils}/bin/true"; + }; + }; + + systemd.services.no-restart-service = { + restartIfChanged = false; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.coreutils}/bin/true"; + }; + }; + }; + restart-and-reload-by-activation-script.configuration = { + imports = [ config.specialisation.service-and-socket.configuration ]; + system.activationScripts.restart-and-reload-test = { + supportsDryActivation = true; + deps = []; + text = '' + if [ "$NIXOS_ACTION" = dry-activate ]; then + f=/run/nixos/dry-activation-restart-list + else + f=/run/nixos/activation-restart-list + fi + cat <> "$f" + simple-service.service + simple-restart-service.service + simple-reload-service.service + no-restart-service.service + socket-activated.service + EOF + ''; + }; + }; + }; }; other = { ... }: { users.mutableUsers = true; }; }; - testScript = {nodes, ...}: let + testScript = { nodes, ... }: let originalSystem = nodes.machine.config.system.build.toplevel; otherSystem = nodes.other.config.system.build.toplevel; @@ -27,12 +147,121 @@ import ./make-test-python.nix ({ pkgs, ...} : { set -o pipefail exec env -i "$@" | tee /dev/stderr ''; - in '' + in /* python */ '' + def switch_to_specialisation(name, action="test"): + out = machine.succeed(f"${originalSystem}/specialisation/{name}/bin/switch-to-configuration {action} 2>&1") + assert_lacks(out, "switch-to-configuration line") # Perl warnings + return out + + def assert_contains(haystack, needle): + if needle not in haystack: + print("The haystack that will cause the following exception is:") + print("---") + print(haystack) + print("---") + raise Exception(f"Expected string '{needle}' was not found") + + def assert_lacks(haystack, needle): + if needle in haystack: + print("The haystack that will cause the following exception is:") + print("---") + print(haystack, end="") + print("---") + raise Exception(f"Unexpected string '{needle}' was found") + + machine.succeed( "${stderrRunner} ${originalSystem}/bin/switch-to-configuration test" ) machine.succeed( "${stderrRunner} ${otherSystem}/bin/switch-to-configuration test" ) + + with subtest("systemd sockets"): + machine.succeed("${originalSystem}/bin/switch-to-configuration test") + + # Simple socket is created + out = switch_to_specialisation("simple-socket") + assert_lacks(out, "stopping the following units:") + # not checking for reload because dbus gets reloaded + assert_lacks(out, "restarting the following units:") + assert_lacks(out, "\nstarting the following units:") + assert_contains(out, "the following new units were started: socket-activated.socket\n") + assert_lacks(out, "as well:") + machine.succeed("[ $(stat -c%a /run/test.sock) = 777 ]") + + # Changing the socket restarts it + out = switch_to_specialisation("modified-socket") + assert_lacks(out, "stopping the following units:") + #assert_lacks(out, "reloading the following units:") + assert_contains(out, "restarting the following units: socket-activated.socket\n") + assert_lacks(out, "\nstarting the following units:") + assert_lacks(out, "the following new units were started:") + assert_lacks(out, "as well:") + machine.succeed("[ $(stat -c%a /run/test.sock) = 666 ]") # change was applied + + # The unit is properly activated when the socket is accessed + if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello": + raise Exception("Socket was not properly activated") + + # Changing the socket restarts it and ignores the active service + out = switch_to_specialisation("simple-socket") + assert_contains(out, "stopping the following units: socket-activated.service\n") + assert_lacks(out, "reloading the following units:") + assert_contains(out, "restarting the following units: socket-activated.socket\n") + assert_lacks(out, "\nstarting the following units:") + assert_lacks(out, "the following new units were started:") + assert_lacks(out, "as well:") + machine.succeed("[ $(stat -c%a /run/test.sock) = 777 ]") # change was applied + + # Changing the service does nothing when the service is not active + out = switch_to_specialisation("modified-service") + assert_lacks(out, "stopping the following units:") + assert_lacks(out, "reloading the following units:") + assert_lacks(out, "restarting the following units:") + assert_lacks(out, "\nstarting the following units:") + assert_lacks(out, "the following new units were started:") + assert_lacks(out, "as well:") + + # Activating the service and modifying it stops it but leaves the socket untouched + machine.succeed("socat - UNIX-CONNECT:/run/test.sock") + out = switch_to_specialisation("simple-socket") + assert_contains(out, "stopping the following units: socket-activated.service\n") + assert_lacks(out, "reloading the following units:") + assert_lacks(out, "restarting the following units:") + assert_lacks(out, "\nstarting the following units:") + assert_lacks(out, "the following new units were started:") + assert_lacks(out, "as well:") + + # Activating the service and both the service and the socket stops the service and restarts the socket + machine.succeed("socat - UNIX-CONNECT:/run/test.sock") + out = switch_to_specialisation("modified-service-and-socket") + assert_contains(out, "stopping the following units: socket-activated.service\n") + assert_lacks(out, "reloading the following units:") + assert_contains(out, "restarting the following units: socket-activated.socket\n") + assert_lacks(out, "\nstarting the following units:") + assert_lacks(out, "the following new units were started:") + assert_lacks(out, "as well:") + + with subtest("restart and reload by activation file"): + out = switch_to_specialisation("service-and-socket") + # Switch to a system where the example services get restarted + # by the activation script + out = switch_to_specialisation("restart-and-reload-by-activation-script") + assert_lacks(out, "stopping the following units:") + assert_contains(out, "stopping the following units as well: simple-service.service, socket-activated.service\n") + assert_contains(out, "reloading the following units: simple-reload-service.service\n") + assert_contains(out, "restarting the following units: simple-restart-service.service\n") + assert_contains(out, "\nstarting the following units: simple-service.service") + + # The same, but in dry mode + switch_to_specialisation("service-and-socket") + out = switch_to_specialisation("restart-and-reload-by-activation-script", action="dry-activate") + assert_lacks(out, "would stop the following units:") + assert_contains(out, "would stop the following units as well: simple-service.service, socket-activated.service\n") + assert_contains(out, "would reload the following units: simple-reload-service.service\n") + assert_contains(out, "would restart the following units: simple-restart-service.service\n") + assert_contains(out, "\nwould start the following units: simple-service.service") + ''; })