1#! @perl@ 2 3use strict; 4use warnings; 5use File::Basename; 6use File::Slurp; 7use Sys::Syslog qw(:standard :macros); 8use Cwd 'abs_path'; 9 10my $out = "@out@"; 11 12# To be robust against interruption, record what units need to be started etc. 13my $startListFile = "/run/systemd/start-list"; 14my $restartListFile = "/run/systemd/restart-list"; 15my $reloadListFile = "/run/systemd/reload-list"; 16 17my $action = shift @ARGV; 18 19if (!defined $action || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) { 20 print STDERR <<EOF; 21Usage: $0 [switch|boot|test] 22 23switch: make the configuration the boot default and activate now 24boot: make the configuration the boot default 25test: activate the configuration, but don\'t make it the boot default 26dry-activate: show what would be done if this configuration were activated 27EOF 28 exit 1; 29} 30 31# This is a NixOS installation if it has /etc/NIXOS or a proper 32# /etc/os-release. 33die "This is not a NixOS installation!\n" unless 34 -f "/etc/NIXOS" || (read_file("/etc/os-release", err_mode => 'quiet') // "") =~ /ID=nixos/s; 35 36openlog("nixos", "", LOG_USER); 37 38# Install or update the bootloader. 39if ($action eq "switch" || $action eq "boot") { 40 system("@installBootLoader@ $out") == 0 or exit 1; 41} 42 43# Just in case the new configuration hangs the system, do a sync now. 44system("@coreutils@/bin/sync") unless ($ENV{"NIXOS_NO_SYNC"} // "") eq "1"; 45 46exit 0 if $action eq "boot"; 47 48# Check if we can activate the new configuration. 49my $oldVersion = read_file("/run/current-system/init-interface-version", err_mode => 'quiet') // ""; 50my $newVersion = read_file("$out/init-interface-version"); 51 52if ($newVersion ne $oldVersion) { 53 print STDERR <<EOF; 54Warning: the new NixOS configuration has an ‘init’ that is 55incompatible with the current configuration. The new configuration 56won\'t take effect until you reboot the system. 57EOF 58 exit 100; 59} 60 61# Ignore SIGHUP so that we're not killed if we're running on (say) 62# virtual console 1 and we restart the "tty1" unit. 63$SIG{PIPE} = "IGNORE"; 64 65sub getActiveUnits { 66 # FIXME: use D-Bus or whatever to query this, since parsing the 67 # output of list-units is likely to break. 68 my $lines = `LANG= systemctl list-units --full --no-legend`; 69 my $res = {}; 70 foreach my $line (split '\n', $lines) { 71 chomp $line; 72 last if $line eq ""; 73 $line =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s/ or next; 74 next if $1 eq "UNIT"; 75 $res->{$1} = { load => $2, state => $3, substate => $4 }; 76 } 77 return $res; 78} 79 80sub parseFstab { 81 my ($filename) = @_; 82 my ($fss, $swaps); 83 foreach my $line (read_file($filename, err_mode => 'quiet')) { 84 chomp $line; 85 $line =~ s/^\s*#.*//; 86 next if $line =~ /^\s*$/; 87 my @xs = split / /, $line; 88 if ($xs[2] eq "swap") { 89 $swaps->{$xs[0]} = { options => $xs[3] // "" }; 90 } else { 91 $fss->{$xs[1]} = { device => $xs[0], fsType => $xs[2], options => $xs[3] // "" }; 92 } 93 } 94 return ($fss, $swaps); 95} 96 97sub parseUnit { 98 my ($filename) = @_; 99 my $info = {}; 100 parseKeyValues($info, read_file($filename)) if -f $filename; 101 parseKeyValues($info, read_file("${filename}.d/overrides.conf")) if -f "${filename}.d/overrides.conf"; 102 return $info; 103} 104 105sub parseKeyValues { 106 my $info = shift; 107 foreach my $line (@_) { 108 # FIXME: not quite correct. 109 $line =~ /^([^=]+)=(.*)$/ or next; 110 $info->{$1} = $2; 111 } 112} 113 114sub boolIsTrue { 115 my ($s) = @_; 116 return $s eq "yes" || $s eq "true"; 117} 118 119sub recordUnit { 120 my ($fn, $unit) = @_; 121 write_file($fn, { append => 1 }, "$unit\n") if $action ne "dry-activate"; 122} 123 124# As a fingerprint for determining whether a unit has changed, we use 125# its absolute path. If it has an override file, we append *its* 126# absolute path as well. 127sub fingerprintUnit { 128 my ($s) = @_; 129 return abs_path($s) . (-f "${s}.d/overrides.conf" ? " " . abs_path "${s}.d/overrides.conf" : ""); 130} 131 132# Figure out what units need to be stopped, started, restarted or reloaded. 133my (%unitsToStop, %unitsToSkip, %unitsToStart, %unitsToRestart, %unitsToReload); 134 135my %unitsToFilter; # units not shown 136 137$unitsToStart{$_} = 1 foreach 138 split('\n', read_file($startListFile, err_mode => 'quiet') // ""); 139 140$unitsToRestart{$_} = 1 foreach 141 split('\n', read_file($restartListFile, err_mode => 'quiet') // ""); 142 143$unitsToReload{$_} = 1 foreach 144 split '\n', read_file($reloadListFile, err_mode => 'quiet') // ""; 145 146my $activePrev = getActiveUnits; 147while (my ($unit, $state) = each %{$activePrev}) { 148 my $baseUnit = $unit; 149 150 # Recognise template instances. 151 $baseUnit = "$1\@.$2" if $unit =~ /^(.*)@[^\.]*\.(.*)$/; 152 my $prevUnitFile = "/etc/systemd/system/$baseUnit"; 153 my $newUnitFile = "$out/etc/systemd/system/$baseUnit"; 154 155 my $baseName = $baseUnit; 156 $baseName =~ s/\.[a-z]*$//; 157 158 if (-e $prevUnitFile && ($state->{state} eq "active" || $state->{state} eq "activating")) { 159 if (! -e $newUnitFile || abs_path($newUnitFile) eq "/dev/null") { 160 my $unitInfo = parseUnit($prevUnitFile); 161 $unitsToStop{$unit} = 1 if boolIsTrue($unitInfo->{'X-StopOnRemoval'} // "yes"); 162 } 163 164 elsif ($unit =~ /\.target$/) { 165 my $unitInfo = parseUnit($newUnitFile); 166 167 # Cause all active target units to be restarted below. 168 # This should start most changed units we stop here as 169 # well as any new dependencies (including new mounts and 170 # swap devices). FIXME: the suspend target is sometimes 171 # active after the system has resumed, which probably 172 # should not be the case. Just ignore it. 173 if ($unit ne "suspend.target" && $unit ne "hibernate.target" && $unit ne "hybrid-sleep.target") { 174 unless (boolIsTrue($unitInfo->{'RefuseManualStart'} // "no")) { 175 $unitsToStart{$unit} = 1; 176 recordUnit($startListFile, $unit); 177 # Don't spam the user with target units that always get started. 178 $unitsToFilter{$unit} = 1; 179 } 180 } 181 182 # Stop targets that have X-StopOnReconfiguration set. 183 # This is necessary to respect dependency orderings 184 # involving targets: if unit X starts after target Y and 185 # target Y starts after unit Z, then if X and Z have both 186 # changed, then X should be restarted after Z. However, 187 # if target Y is in the "active" state, X and Z will be 188 # restarted at the same time because X's dependency on Y 189 # is already satisfied. Thus, we need to stop Y first. 190 # Stopping a target generally has no effect on other units 191 # (unless there is a PartOf dependency), so this is just a 192 # bookkeeping thing to get systemd to do the right thing. 193 if (boolIsTrue($unitInfo->{'X-StopOnReconfiguration'} // "no")) { 194 $unitsToStop{$unit} = 1; 195 } 196 } 197 198 elsif (fingerprintUnit($prevUnitFile) ne fingerprintUnit($newUnitFile)) { 199 if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target") { 200 # Do nothing. These cannot be restarted directly. 201 } elsif ($unit =~ /\.mount$/) { 202 # Reload the changed mount unit to force a remount. 203 $unitsToReload{$unit} = 1; 204 recordUnit($reloadListFile, $unit); 205 } elsif ($unit =~ /\.socket$/ || $unit =~ /\.path$/ || $unit =~ /\.slice$/) { 206 # FIXME: do something? 207 } else { 208 my $unitInfo = parseUnit($newUnitFile); 209 if (boolIsTrue($unitInfo->{'X-ReloadIfChanged'} // "no")) { 210 $unitsToReload{$unit} = 1; 211 recordUnit($reloadListFile, $unit); 212 } 213 elsif (!boolIsTrue($unitInfo->{'X-RestartIfChanged'} // "yes") || boolIsTrue($unitInfo->{'RefuseManualStop'} // "no") ) { 214 $unitsToSkip{$unit} = 1; 215 } else { 216 # If this unit is socket-activated, then stop the 217 # socket unit(s) as well, and restart the 218 # socket(s) instead of the service. 219 my $socketActivated = 0; 220 if ($unit =~ /\.service$/) { 221 my @sockets = split / /, ($unitInfo->{Sockets} // ""); 222 if (scalar @sockets == 0) { 223 @sockets = ("$baseName.socket"); 224 } 225 foreach my $socket (@sockets) { 226 if (defined $activePrev->{$socket}) { 227 $unitsToStop{$unit} = 1; 228 $unitsToStart{$unit} = 1; 229 recordUnit($startListFile, $socket); 230 $socketActivated = 1; 231 } 232 } 233 } 234 235 if (!boolIsTrue($unitInfo->{'X-StopIfChanged'} // "yes")) { 236 237 # This unit should be restarted instead of 238 # stopped and started. 239 $unitsToRestart{$unit} = 1; 240 recordUnit($restartListFile, $unit); 241 242 } else { 243 244 # If the unit is not socket-activated, record 245 # that this unit needs to be started below. 246 # We write this to a file to ensure that the 247 # service gets restarted if we're interrupted. 248 if (!$socketActivated) { 249 $unitsToStart{$unit} = 1; 250 recordUnit($startListFile, $unit); 251 } 252 253 $unitsToStop{$unit} = 1; 254 255 } 256 } 257 } 258 } 259 } 260} 261 262sub pathToUnitName { 263 my ($path) = @_; 264 open my $cmd, "-|", "systemd-escape", "--suffix=mount", "-p", $path 265 or die "Unable to escape $path!\n"; 266 my $escaped = join "", <$cmd>; 267 chomp $escaped; 268 close $cmd or die; 269 return $escaped; 270} 271 272sub unique { 273 my %seen; 274 my @res; 275 foreach my $name (@_) { 276 next if $seen{$name}; 277 $seen{$name} = 1; 278 push @res, $name; 279 } 280 return @res; 281} 282 283# Compare the previous and new fstab to figure out which filesystems 284# need a remount or need to be unmounted. New filesystems are mounted 285# automatically by starting local-fs.target. FIXME: might be nicer if 286# we generated units for all mounts; then we could unify this with the 287# unit checking code above. 288my ($prevFss, $prevSwaps) = parseFstab "/etc/fstab"; 289my ($newFss, $newSwaps) = parseFstab "$out/etc/fstab"; 290foreach my $mountPoint (keys %$prevFss) { 291 my $prev = $prevFss->{$mountPoint}; 292 my $new = $newFss->{$mountPoint}; 293 my $unit = pathToUnitName($mountPoint); 294 if (!defined $new) { 295 # Filesystem entry disappeared, so unmount it. 296 $unitsToStop{$unit} = 1; 297 } elsif ($prev->{fsType} ne $new->{fsType} || $prev->{device} ne $new->{device}) { 298 # Filesystem type or device changed, so unmount and mount it. 299 $unitsToStop{$unit} = 1; 300 $unitsToStart{$unit} = 1; 301 recordUnit($startListFile, $unit); 302 } elsif ($prev->{options} ne $new->{options}) { 303 # Mount options changes, so remount it. 304 $unitsToReload{$unit} = 1; 305 recordUnit($reloadListFile, $unit); 306 } 307} 308 309# Also handles swap devices. 310foreach my $device (keys %$prevSwaps) { 311 my $prev = $prevSwaps->{$device}; 312 my $new = $newSwaps->{$device}; 313 if (!defined $new) { 314 # Swap entry disappeared, so turn it off. Can't use 315 # "systemctl stop" here because systemd has lots of alias 316 # units that prevent a stop from actually calling 317 # "swapoff". 318 print STDERR "stopping swap device: $device\n"; 319 system("@utillinux@/sbin/swapoff", $device); 320 } 321 # FIXME: update swap options (i.e. its priority). 322} 323 324 325# Should we have systemd re-exec itself? 326my $prevSystemd = abs_path("/proc/1/exe") // "/unknown"; 327my $newSystemd = abs_path("@systemd@/lib/systemd/systemd") or die; 328my $restartSystemd = $prevSystemd ne $newSystemd; 329 330 331sub filterUnits { 332 my ($units) = @_; 333 my @res; 334 foreach my $unit (sort(keys %{$units})) { 335 push @res, $unit if !defined $unitsToFilter{$unit}; 336 } 337 return @res; 338} 339 340my @unitsToStopFiltered = filterUnits(\%unitsToStop); 341my @unitsToStartFiltered = filterUnits(\%unitsToStart); 342 343 344# Show dry-run actions. 345if ($action eq "dry-activate") { 346 print STDERR "would stop the following units: ", join(", ", @unitsToStopFiltered), "\n" 347 if scalar @unitsToStopFiltered > 0; 348 print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n" 349 if scalar(keys %unitsToSkip) > 0; 350 print STDERR "would restart systemd\n" if $restartSystemd; 351 print STDERR "would restart the following units: ", join(", ", sort(keys %unitsToRestart)), "\n" 352 if scalar(keys %unitsToRestart) > 0; 353 print STDERR "would start the following units: ", join(", ", @unitsToStartFiltered), "\n" 354 if scalar @unitsToStartFiltered; 355 print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n" 356 if scalar(keys %unitsToReload) > 0; 357 exit 0; 358} 359 360 361syslog(LOG_NOTICE, "switching to system configuration $out"); 362 363if (scalar (keys %unitsToStop) > 0) { 364 print STDERR "stopping the following units: ", join(", ", @unitsToStopFiltered), "\n" 365 if scalar @unitsToStopFiltered; 366 system("systemctl", "stop", "--", sort(keys %unitsToStop)); # FIXME: ignore errors? 367} 368 369print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n" 370 if scalar(keys %unitsToSkip) > 0; 371 372# Activate the new configuration (i.e., update /etc, make accounts, 373# and so on). 374my $res = 0; 375print STDERR "activating the configuration...\n"; 376system("$out/activate", "$out") == 0 or $res = 2; 377 378# Restart systemd if necessary. 379if ($restartSystemd) { 380 print STDERR "restarting systemd...\n"; 381 system("@systemd@/bin/systemctl", "daemon-reexec") == 0 or $res = 2; 382} 383 384# Forget about previously failed services. 385system("@systemd@/bin/systemctl", "reset-failed"); 386 387# Make systemd reload its units. 388system("@systemd@/bin/systemctl", "daemon-reload") == 0 or $res = 3; 389 390# Reload units that need it. This includes remounting changed mount 391# units. 392if (scalar(keys %unitsToReload) > 0) { 393 print STDERR "reloading the following units: ", join(", ", sort(keys %unitsToReload)), "\n"; 394 system("@systemd@/bin/systemctl", "reload", "--", sort(keys %unitsToReload)) == 0 or $res = 4; 395 unlink($reloadListFile); 396} 397 398# Restart changed services (those that have to be restarted rather 399# than stopped and started). 400if (scalar(keys %unitsToRestart) > 0) { 401 print STDERR "restarting the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"; 402 system("@systemd@/bin/systemctl", "restart", "--", sort(keys %unitsToRestart)) == 0 or $res = 4; 403 unlink($restartListFile); 404} 405 406# Start all active targets, as well as changed units we stopped above. 407# The latter is necessary because some may not be dependencies of the 408# targets (i.e., they were manually started). FIXME: detect units 409# that are symlinks to other units. We shouldn't start both at the 410# same time because we'll get a "Failed to add path to set" error from 411# systemd. 412print STDERR "starting the following units: ", join(", ", @unitsToStartFiltered), "\n" 413 if scalar @unitsToStartFiltered; 414system("@systemd@/bin/systemctl", "start", "--", sort(keys %unitsToStart)) == 0 or $res = 4; 415unlink($startListFile); 416 417 418# Print failed and new units. 419my (@failed, @new, @restarting); 420my $activeNew = getActiveUnits; 421while (my ($unit, $state) = each %{$activeNew}) { 422 if ($state->{state} eq "failed") { 423 push @failed, $unit; 424 } 425 elsif ($state->{state} eq "auto-restart") { 426 # A unit in auto-restart state is a failure *if* it previously failed to start 427 my $lines = `@systemd@/bin/systemctl show '$unit'`; 428 my $info = {}; 429 parseKeyValues($info, split("\n", $lines)); 430 431 if ($info->{ExecMainStatus} ne '0') { 432 push @failed, $unit; 433 } 434 } 435 elsif ($state->{state} ne "failed" && !defined $activePrev->{$unit}) { 436 push @new, $unit; 437 } 438} 439 440print STDERR "the following new units were started: ", join(", ", sort(@new)), "\n" 441 if scalar @new > 0; 442 443if (scalar @failed > 0) { 444 print STDERR "warning: the following units failed: ", join(", ", sort(@failed)), "\n"; 445 foreach my $unit (@failed) { 446 print STDERR "\n"; 447 system("COLUMNS=1000 @systemd@/bin/systemctl status --no-pager '$unit' >&2"); 448 } 449 $res = 4; 450} 451 452if ($res == 0) { 453 syslog(LOG_NOTICE, "finished switching to system configuration $out"); 454} else { 455 syslog(LOG_ERR, "switching to system configuration $out failed (status $res)"); 456} 457 458exit $res;