diff options
author | Janne Heß <janne@hess.ooo> | 2022-01-29 22:56:52 +0100 |
---|---|---|
committer | Janne Heß <janne@hess.ooo> | 2022-02-09 14:31:45 +0100 |
commit | b9bb1de34121312c4c37829b8ee10c81ffd2187a (patch) | |
tree | 72651f364cb4026c35541ebc207fd6c1090f2cdd /nixos/modules/system/activation | |
parent | 78db7b6529a12f2afbac4d3958b68c23b04e8996 (diff) | |
download | nixpkgs-b9bb1de34121312c4c37829b8ee10c81ffd2187a.tar nixpkgs-b9bb1de34121312c4c37829b8ee10c81ffd2187a.tar.gz nixpkgs-b9bb1de34121312c4c37829b8ee10c81ffd2187a.tar.bz2 nixpkgs-b9bb1de34121312c4c37829b8ee10c81ffd2187a.tar.lz nixpkgs-b9bb1de34121312c4c37829b8ee10c81ffd2187a.tar.xz nixpkgs-b9bb1de34121312c4c37829b8ee10c81ffd2187a.tar.zst nixpkgs-b9bb1de34121312c4c37829b8ee10c81ffd2187a.zip |
nixos/switch-to-configuration: Implement reload support
This is accomplished by comparing the hashes that the unit files contain. By filtering for a special key `X-Reload-Triggers` in the `[Unit]` section, we can differentiate between reloads and restarts. Since activation scripts can request reloads of units as well, more checking of this behaviour is implemented. If a unit is to be restarted, it's never reloaded as well which would make no sense. Also removes a useless subroutine and perl dependencies that are nowadays handled by the propagated build inputs feature of `perl.withPackages`.
Diffstat (limited to 'nixos/modules/system/activation')
-rw-r--r-- | nixos/modules/system/activation/switch-to-configuration.pl | 161 | ||||
-rw-r--r-- | nixos/modules/system/activation/top-level.nix | 2 |
2 files changed, 140 insertions, 23 deletions
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl index f2c1cf795d1..2fe78634d62 100644 --- a/nixos/modules/system/activation/switch-to-configuration.pl +++ b/nixos/modules/system/activation/switch-to-configuration.pl @@ -2,10 +2,11 @@ use strict; use warnings; +use Array::Compare; use Config::IniFiles; use File::Path qw(make_path); use File::Basename; -use File::Slurp; +use File::Slurp qw(read_file write_file edit_file); use Net::DBus; use Sys::Syslog qw(:standard :macros); use Cwd 'abs_path'; @@ -20,12 +21,19 @@ my $restartListFile = "/run/nixos/restart-list"; my $reloadListFile = "/run/nixos/reload-list"; # Parse restart/reload requests by the activation script. -# Activation scripts may write newline-separated units to this +# Activation scripts may write newline-separated units to the restart # file and switch-to-configuration will handle them. While # `stopIfChanged = true` is ignored, switch-to-configuration will # handle `restartIfChanged = false` and `reloadIfChanged = true`. +# This is the same as specifying a restart trigger in the NixOS module. +# +# The reload file asks the script to reload a unit. This is the same as +# specifying a reload trigger in the NixOS module and can be ignored if +# the unit is restarted in this activation. 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) }); @@ -196,12 +204,88 @@ sub recordUnit { write_file($fn, { append => 1 }, "$unit\n") if $action ne "dry-activate"; } -# As a fingerprint for determining whether a unit has changed, we use -# its absolute path. If it has an override file, we append *its* -# absolute path as well. -sub fingerprintUnit { - my ($s) = @_; - return abs_path($s) . (-f "${s}.d/overrides.conf" ? " " . abs_path "${s}.d/overrides.conf" : ""); +# The opposite of recordUnit, removes a unit name from a file +sub unrecord_unit { + my ($fn, $unit) = @_; + edit_file { s/^$unit\n//msx } $fn if $action ne "dry-activate"; +} + +# Compare the contents of two unit files and return whether the unit +# needs to be restarted or reloaded. If the units differ, the service +# is restarted unless the only difference is `X-Reload-Triggers` in the +# `Unit` section. If this is the only modification, the unit is reloaded +# instead of restarted. +# Returns: +# - 0 if the units are equal +# - 1 if the units are different and a restart action is required +# - 2 if the units are different and a reload action is required +sub compare_units { + my ($old_unit, $new_unit) = @_; + my $comp = Array::Compare->new; + my $ret = 0; + + # Comparison hash for the sections + my %section_cmp = map { $_ => 1 } keys %{$new_unit}; + # Iterate over the sections + foreach my $section_name (keys %{$old_unit}) { + # Missing section in the new unit? + if (not exists $section_cmp{$section_name}) { + if ($section_name eq 'Unit' and %{$old_unit->{'Unit'}} == 1 and defined(%{$old_unit->{'Unit'}}{'X-Reload-Triggers'})) { + # If a new [Unit] section was removed that only contained X-Reload-Triggers, + # do nothing. + next; + } else { + return 1; + } + } + delete $section_cmp{$section_name}; + # Comparison hash for the section contents + my %ini_cmp = map { $_ => 1 } keys %{$new_unit->{$section_name}}; + # Iterate over the keys of the section + foreach my $ini_key (keys %{$old_unit->{$section_name}}) { + delete $ini_cmp{$ini_key}; + my @old_value = @{$old_unit->{$section_name}{$ini_key}}; + # If the key is missing in the new unit, they are different... + if (not $new_unit->{$section_name}{$ini_key}) { + # ... unless the key that is now missing was the reload trigger + if ($section_name eq 'Unit' and $ini_key eq 'X-Reload-Triggers') { + next; + } + return 1; + } + my @new_value = @{$new_unit->{$section_name}{$ini_key}}; + # If the contents are different, the units are different + if (not $comp->compare(\@old_value, \@new_value)) { + # Check if only the reload triggers changed + if ($section_name eq 'Unit' and $ini_key eq 'X-Reload-Triggers') { + $ret = 2; + } else { + return 1; + } + } + } + # A key was introduced that was missing in the old unit + if (%ini_cmp) { + if ($section_name eq 'Unit' and %ini_cmp == 1 and defined($ini_cmp{'X-Reload-Triggers'})) { + # If the newly introduced key was the reload triggers, reload the unit + $ret = 2; + } else { + return 1; + } + }; + } + # A section was introduced that was missing in the old unit + if (%section_cmp) { + if (%section_cmp == 1 and defined($section_cmp{'Unit'}) and %{$new_unit->{'Unit'}} == 1 and defined(%{$new_unit->{'Unit'}}{'X-Reload-Triggers'})) { + # If a new [Unit] section was introduced that only contains X-Reload-Triggers, + # reload instead of restarting + $ret = 2; + } else { + return 1; + } + } + + return $ret; } sub handleModifiedUnit { @@ -224,7 +308,7 @@ sub handleModifiedUnit { # More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430 } else { my %unitInfo = $newUnitInfo ? %{$newUnitInfo} : parseUnit($newUnitFile); - if (parseSystemdBool(\%unitInfo, "Service", "X-ReloadIfChanged", 0)) { + if (parseSystemdBool(\%unitInfo, "Service", "X-ReloadIfChanged", 0) and not $unitsToRestart->{$unit} and not $unitsToStop->{$unit}) { $unitsToReload->{$unit} = 1; recordUnit($reloadListFile, $unit); } @@ -238,6 +322,11 @@ sub handleModifiedUnit { # stopped and started. $unitsToRestart->{$unit} = 1; recordUnit($restartListFile, $unit); + # Remove from units to reload so we don't restart and reload + if ($unitsToReload->{$unit}) { + delete $unitsToReload->{$unit}; + unrecord_unit($reloadListFile, $unit); + } } else { # If this unit is socket-activated, then stop the # socket unit(s) as well, and restart the @@ -258,6 +347,11 @@ sub handleModifiedUnit { recordUnit($startListFile, $socket); $socketActivated = 1; } + # Remove from units to reload so we don't restart and reload + if ($unitsToReload->{$unit}) { + delete $unitsToReload->{$unit}; + unrecord_unit($reloadListFile, $unit); + } } } } @@ -272,6 +366,11 @@ sub handleModifiedUnit { } $unitsToStop->{$unit} = 1; + # Remove from units to reload so we don't restart and reload + if ($unitsToReload->{$unit}) { + delete $unitsToReload->{$unit}; + unrecord_unit($reloadListFile, $unit); + } } } } @@ -348,8 +447,16 @@ while (my ($unit, $state) = each %{$activePrev}) { } } - elsif (fingerprintUnit($prevUnitFile) ne fingerprintUnit($newUnitFile)) { - handleModifiedUnit($unit, $baseName, $newUnitFile, undef, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip); + else { + my %old_unit_info = parseUnit($prevUnitFile); + my %new_unit_info = parseUnit($newUnitFile); + my $diff = compare_units(\%old_unit_info, \%new_unit_info); + if ($diff eq 1) { + handleModifiedUnit($unit, $baseName, $newUnitFile, \%new_unit_info, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip); + } elsif ($diff eq 2 and not $unitsToRestart{$unit}) { + $unitsToReload{$unit} = 1; + recordUnit($reloadListFile, $unit); + } } } } @@ -365,17 +472,6 @@ sub pathToUnitName { return $escaped; } -sub unique { - my %seen; - my @res; - foreach my $name (@_) { - next if $seen{$name}; - $seen{$name} = 1; - push @res, $name; - } - return @res; -} - # Compare the previous and new fstab to figure out which filesystems # need a remount or need to be unmounted. New filesystems are mounted # automatically by starting local-fs.target. FIXME: might be nicer if @@ -477,6 +573,16 @@ if ($action eq "dry-activate") { } unlink($dryRestartByActivationFile); + foreach (split('\n', read_file($dryReloadByActivationFile, err_mode => 'quiet') // "")) { + my $unit = $_; + + if (defined($activePrev->{$unit}) and not $unitsToRestart{$unit} and not $unitsToStop{$unit}) { + $unitsToReload{$unit} = 1; + recordUnit($reloadListFile, $unit); + } + } + unlink($dryReloadByActivationFile); + print STDERR "would restart systemd\n" if $restartSystemd; print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n" if scalar(keys %unitsToReload) > 0; @@ -534,6 +640,17 @@ foreach (split('\n', read_file($restartByActivationFile, err_mode => 'quiet') // # We can remove the file now because it has been propagated to the other restart/reload files unlink($restartByActivationFile); +foreach (split('\n', read_file($reloadByActivationFile, err_mode => 'quiet') // "")) { + my $unit = $_; + + if (defined($activePrev->{$unit}) and not $unitsToRestart{$unit} and not $unitsToStop{$unit}) { + $unitsToReload{$unit} = 1; + recordUnit($reloadListFile, $unit); + } +} +# We can remove the file now because it has been propagated to the other reload file +unlink($reloadByActivationFile); + # Restart systemd if necessary. Note that this is done using the # current version of systemd, just in case the new one has trouble # communicating with the running pid 1. diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix index 9e6ca75b9da..4745239050b 100644 --- a/nixos/modules/system/activation/top-level.nix +++ b/nixos/modules/system/activation/top-level.nix @@ -117,7 +117,7 @@ let configurationName = config.boot.loader.grub.configurationName; # Needed by switch-to-configuration. - perl = pkgs.perl.withPackages (p: with p; [ FileSlurp NetDBus XMLParser XMLTwig ConfigIniFiles ]); + perl = pkgs.perl.withPackages (p: with p; [ ArrayCompare ConfigIniFiles FileSlurp NetDBus ]); }; # Handle assertions and warnings |