···
3
+
collections::HashMap,
4
+
io::{BufRead, Write},
5
+
os::unix::{fs::PermissionsExt, process::CommandExt},
6
+
path::{Path, PathBuf},
13
+
use anyhow::{anyhow, bail, Context, Result};
15
+
blocking::{stdintf::org_freedesktop_dbus::Properties, LocalConnection, Proxy},
20
+
use log::LevelFilter;
22
+
fcntl::{Flock, FlockArg, OFlag},
24
+
signal::{self, SigHandler, Signal},
29
+
use syslog::Facility;
31
+
mod systemd_manager {
32
+
#![allow(non_upper_case_globals)]
33
+
#![allow(non_camel_case_types)]
34
+
#![allow(non_snake_case)]
36
+
include!(concat!(env!("OUT_DIR"), "/systemd_manager.rs"));
39
+
mod logind_manager {
40
+
#![allow(non_upper_case_globals)]
41
+
#![allow(non_camel_case_types)]
42
+
#![allow(non_snake_case)]
44
+
include!(concat!(env!("OUT_DIR"), "/logind_manager.rs"));
47
+
use crate::systemd_manager::OrgFreedesktopSystemd1Manager;
49
+
logind_manager::OrgFreedesktopLogin1Manager,
51
+
OrgFreedesktopSystemd1ManagerJobRemoved, OrgFreedesktopSystemd1ManagerReloading,
55
+
type UnitInfo = HashMap<String, HashMap<String, Vec<String>>>;
57
+
const SYSINIT_REACTIVATION_TARGET: &str = "sysinit-reactivation.target";
59
+
// To be robust against interruption, record what units need to be started etc. We read these files
60
+
// again every time this program starts to make sure we continue where the old (interrupted) script
62
+
const START_LIST_FILE: &str = "/run/nixos/start-list";
63
+
const RESTART_LIST_FILE: &str = "/run/nixos/restart-list";
64
+
const RELOAD_LIST_FILE: &str = "/run/nixos/reload-list";
66
+
// Parse restart/reload requests by the activation script. Activation scripts may write
67
+
// newline-separated units to the restart file and switch-to-configuration will handle them. While
68
+
// `stopIfChanged = true` is ignored, switch-to-configuration will handle `restartIfChanged =
69
+
// false` and `reloadIfChanged = true`. This is the same as specifying a restart trigger in the
72
+
// The reload file asks this program to reload a unit. This is the same as specifying a reload
73
+
// trigger in the NixOS module and can be ignored if the unit is restarted in this activation.
74
+
const RESTART_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/activation-restart-list";
75
+
const RELOAD_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/activation-reload-list";
76
+
const DRY_RESTART_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/dry-activation-restart-list";
77
+
const DRY_RELOAD_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/dry-activation-reload-list";
79
+
#[derive(Debug, Clone, PartialEq)]
87
+
impl std::str::FromStr for Action {
88
+
type Err = anyhow::Error;
90
+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
92
+
"switch" => Self::Switch,
93
+
"boot" => Self::Boot,
94
+
"test" => Self::Test,
95
+
"dry-activate" => Self::DryActivate,
96
+
_ => bail!("invalid action {s}"),
101
+
impl Into<&'static str> for &Action {
102
+
fn into(self) -> &'static str {
104
+
Action::Switch => "switch",
105
+
Action::Boot => "boot",
106
+
Action::Test => "test",
107
+
Action::DryActivate => "dry-activate",
112
+
// Allow for this switch-to-configuration to remain consistent with the perl implementation.
113
+
// Perl's "die" uses errno to set the exit code: https://perldoc.perl.org/perlvar#%24%21
115
+
std::process::exit(std::io::Error::last_os_error().raw_os_error().unwrap_or(1));
118
+
fn parse_os_release() -> Result<HashMap<String, String>> {
119
+
Ok(std::fs::read_to_string("/etc/os-release")
120
+
.context("Failed to read /etc/os-release")?
123
+
.fold(HashMap::new(), |mut acc, line| {
124
+
if let Some((k, v)) = line.split_once('=') {
125
+
acc.insert(k.to_string(), v.to_string());
132
+
fn do_install_bootloader(command: &str, toplevel: &Path) -> Result<()> {
133
+
let mut cmd_split = command.split_whitespace();
134
+
let Some(argv0) = cmd_split.next() else {
135
+
bail!("missing first argument in install bootloader commands");
138
+
match std::process::Command::new(argv0)
139
+
.args(cmd_split.collect::<Vec<&str>>())
142
+
.map(|mut child| child.wait())
144
+
Ok(Ok(status)) if status.success() => {}
146
+
eprintln!("Failed to install bootloader");
154
+
extern "C" fn handle_sigpipe(_signal: nix::libc::c_int) {}
156
+
fn required_env(var: &str) -> anyhow::Result<String> {
157
+
std::env::var(var).with_context(|| format!("missing required environment variable ${var}"))
166
+
// Asks the currently running systemd instance via dbus which units are active. Returns a hash
167
+
// where the key is the name of each unit and the value a hash of load, state, substate.
168
+
fn get_active_units<'a>(
169
+
systemd_manager: &Proxy<'a, &LocalConnection>,
170
+
) -> Result<HashMap<String, UnitState>> {
171
+
let units = systemd_manager
172
+
.list_units_by_patterns(Vec::new(), Vec::new())
173
+
.context("Failed to list systemd units")?;
190
+
if following == "" && active_state != "inactive" {
191
+
Some((id, active_state, sub_state))
197
+
.fold(HashMap::new(), |mut acc, (id, active_state, sub_state)| {
201
+
state: active_state,
202
+
substate: sub_state,
210
+
// This function takes a single ini file that specified systemd configuration like unit
211
+
// configuration and parses it into a HashMap where the keys are the sections of the unit file and
212
+
// the values are HashMaps themselves. These HashMaps have the unit file keys as their keys (left
213
+
// side of =) and an array of all values that were set as their values. If a value is empty (for
214
+
// example `ExecStart=`), then all current definitions are removed.
216
+
// Instead of returning the HashMap, this function takes a mutable reference to a HashMap to return
217
+
// the data in. This allows calling the function multiple times with the same Hashmap to parse
219
+
fn parse_systemd_ini(data: &mut UnitInfo, unit_file: &Path) -> Result<()> {
220
+
let ini = Ini::load_from_file(unit_file)
221
+
.with_context(|| format!("Failed to load unit file {}", unit_file.display()))?;
223
+
// Copy over all sections
224
+
for (section, properties) in ini.iter() {
225
+
let Some(section) = section else {
229
+
if section == "Install" {
230
+
// Skip the [Install] section because it has no relevant keys for us
234
+
let section_map = if let Some(section_map) = data.get_mut(section) {
237
+
data.insert(section.to_string(), HashMap::new());
238
+
data.get_mut(section)
239
+
.ok_or(anyhow!("section name should exist in hashmap"))?
242
+
for (ini_key, _) in properties {
243
+
let values = properties.get_all(ini_key);
244
+
let values = values
247
+
.collect::<Vec<String>>();
249
+
// Go over all values
250
+
let mut new_vals = Vec::new();
251
+
let mut clear_existing = false;
253
+
for val in values {
254
+
// If a value is empty, it's an override that tells us to clean the value
255
+
if val.is_empty() {
257
+
clear_existing = true;
259
+
new_vals.push(val);
263
+
match (section_map.get_mut(ini_key), clear_existing) {
264
+
(Some(existing_vals), false) => existing_vals.extend(new_vals),
265
+
_ => _ = section_map.insert(ini_key.to_string(), new_vals),
273
+
// This function takes the path to a systemd configuration file (like a unit configuration) and
274
+
// parses it into a UnitInfo structure.
276
+
// If a directory with the same basename ending in .d exists next to the unit file, it will be
277
+
// assumed to contain override files which will be parsed as well and handled properly.
278
+
fn parse_unit(unit_file: &Path, base_unit_file: &Path) -> Result<UnitInfo> {
279
+
// Parse the main unit and all overrides
280
+
let mut unit_data = HashMap::new();
282
+
parse_systemd_ini(&mut unit_data, base_unit_file)?;
285
+
glob(&format!("{}.d/*.conf", base_unit_file.display())).context("Invalid glob pattern")?
287
+
let Ok(entry) = entry else {
291
+
parse_systemd_ini(&mut unit_data, &entry)?;
294
+
// Handle drop-in template-unit instance overrides
295
+
if unit_file != base_unit_file {
297
+
glob(&format!("{}.d/*.conf", unit_file.display())).context("Invalid glob pattern")?
299
+
let Ok(entry) = entry else {
303
+
parse_systemd_ini(&mut unit_data, &entry)?;
310
+
// Checks whether a specified boolean in a systemd unit is true or false, with a default that is
311
+
// applied when the value is not set.
312
+
fn parse_systemd_bool(
313
+
unit_data: Option<&UnitInfo>,
314
+
section_name: &str,
318
+
if let Some(Some(Some(Some(b)))) = unit_data.map(|data| {
319
+
data.get(section_name).map(|section| {
320
+
section.get(bool_name).map(|vals| {
322
+
.map(|last| matches!(last.as_str(), "1" | "yes" | "true" | "on"))
332
+
#[derive(Debug, PartialEq)]
333
+
enum UnitComparison {
335
+
UnequalNeedsRestart,
336
+
UnequalNeedsReload,
339
+
// Compare the contents of two unit files and return whether the unit needs to be restarted or
340
+
// reloaded. If the units differ, the service is restarted unless the only difference is
341
+
// `X-Reload-Triggers` in the `Unit` section. If this is the only modification, the unit is
342
+
// reloaded instead of restarted. If the only difference is `Options` in the `[Mount]` section, the
343
+
// unit is reloaded rather than restarted.
344
+
fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison {
345
+
let mut ret = UnitComparison::Equal;
347
+
let unit_section_ignores = HashMap::from(
349
+
"X-Reload-Triggers",
354
+
"OnFailureJobMode",
356
+
"StopWhenUnneeded",
357
+
"RefuseManualStart",
358
+
"RefuseManualStop",
363
+
.map(|name| (name, ())),
366
+
let mut section_cmp = new_unit.keys().fold(HashMap::new(), |mut acc, key| {
367
+
acc.insert(key.as_str(), ());
371
+
// Iterate over the sections
372
+
for (section_name, section_val) in current_unit {
373
+
// Missing section in the new unit?
374
+
if !section_cmp.contains_key(section_name.as_str()) {
375
+
// If the [Unit] section was removed, make sure that only keys were in it that are
377
+
if section_name == "Unit" {
378
+
for (ini_key, _ini_val) in section_val {
379
+
if !unit_section_ignores.contains_key(ini_key.as_str()) {
380
+
return UnitComparison::UnequalNeedsRestart;
383
+
continue; // check the next section
385
+
return UnitComparison::UnequalNeedsRestart;
389
+
section_cmp.remove(section_name.as_str());
391
+
// Comparison hash for the section contents
392
+
let mut ini_cmp = new_unit
394
+
.map(|section_val| {
395
+
section_val.keys().fold(HashMap::new(), |mut acc, ini_key| {
396
+
acc.insert(ini_key.as_str(), ());
400
+
.unwrap_or_default();
402
+
// Iterate over the keys of the section
403
+
for (ini_key, current_value) in section_val {
404
+
ini_cmp.remove(ini_key.as_str());
405
+
let Some(Some(new_value)) = new_unit
407
+
.map(|section| section.get(ini_key))
409
+
// If the key is missing in the new unit, they are different unless the key that is
410
+
// now missing is one of the ignored keys
411
+
if section_name == "Unit" && unit_section_ignores.contains_key(ini_key.as_str()) {
414
+
return UnitComparison::UnequalNeedsRestart;
417
+
// If the contents are different, the units are different
418
+
if current_value != new_value {
419
+
if section_name == "Unit" {
420
+
if ini_key == "X-Reload-Triggers" {
421
+
ret = UnitComparison::UnequalNeedsReload;
423
+
} else if unit_section_ignores.contains_key(ini_key.as_str()) {
428
+
// If this is a mount unit, check if it was only `Options`
429
+
if section_name == "Mount" && ini_key == "Options" {
430
+
ret = UnitComparison::UnequalNeedsReload;
434
+
return UnitComparison::UnequalNeedsRestart;
438
+
// A key was introduced that was missing in the previous unit
439
+
if !ini_cmp.is_empty() {
440
+
if section_name == "Unit" {
441
+
for (ini_key, _) in ini_cmp {
442
+
if ini_key == "X-Reload-Triggers" {
443
+
ret = UnitComparison::UnequalNeedsReload;
444
+
} else if unit_section_ignores.contains_key(ini_key) {
447
+
return UnitComparison::UnequalNeedsRestart;
451
+
return UnitComparison::UnequalNeedsRestart;
456
+
// A section was introduced that was missing in the previous unit
457
+
if !section_cmp.is_empty() {
458
+
if section_cmp.keys().len() == 1 && section_cmp.contains_key("Unit") {
459
+
if let Some(new_unit_unit) = new_unit.get("Unit") {
460
+
for (ini_key, _) in new_unit_unit {
461
+
if !unit_section_ignores.contains_key(ini_key.as_str()) {
462
+
return UnitComparison::UnequalNeedsRestart;
463
+
} else if ini_key == "X-Reload-Triggers" {
464
+
ret = UnitComparison::UnequalNeedsReload;
469
+
return UnitComparison::UnequalNeedsRestart;
476
+
// Called when a unit exists in both the old systemd and the new system and the units differ. This
477
+
// figures out of what units are to be stopped, restarted, reloaded, started, and skipped.
478
+
fn handle_modified_unit(
482
+
new_unit_file: &Path,
483
+
new_base_unit_file: &Path,
484
+
new_unit_info: Option<&UnitInfo>,
485
+
active_cur: &HashMap<String, UnitState>,
486
+
units_to_stop: &mut HashMap<String, ()>,
487
+
units_to_start: &mut HashMap<String, ()>,
488
+
units_to_reload: &mut HashMap<String, ()>,
489
+
units_to_restart: &mut HashMap<String, ()>,
490
+
units_to_skip: &mut HashMap<String, ()>,
492
+
let use_restart_as_stop_and_start = new_unit_info.is_none();
496
+
"sysinit.target" | "basic.target" | "multi-user.target" | "graphical.target"
497
+
) || unit.ends_with(".unit")
498
+
|| unit.ends_with(".slice")
500
+
// Do nothing. These cannot be restarted directly.
502
+
// Slices and Paths don't have to be restarted since properties (resource limits and
503
+
// inotify watches) seem to get applied on daemon-reload.
504
+
} else if unit.ends_with(".mount") {
505
+
// Just restart the unit. We wouldn't have gotten into this subroutine if only `Options`
506
+
// was changed, in which case the unit would be reloaded. The only exception is / and /nix
507
+
// because it's very unlikely we can safely unmount them so we reload them instead. This
508
+
// means that we may not get all changes into the running system but it's better than
510
+
if unit == "-.mount" || unit == "nix.mount" {
511
+
units_to_reload.insert(unit.to_string(), ());
512
+
record_unit(RELOAD_LIST_FILE, unit);
514
+
units_to_restart.insert(unit.to_string(), ());
515
+
record_unit(RESTART_LIST_FILE, unit);
517
+
} else if unit.ends_with(".socket") {
518
+
// FIXME: do something?
519
+
// Attempt to fix this: https://github.com/NixOS/nixpkgs/pull/141192
520
+
// Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
521
+
// More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
523
+
let fallback = parse_unit(new_unit_file, new_base_unit_file)?;
524
+
let new_unit_info = if new_unit_info.is_some() {
530
+
if parse_systemd_bool(new_unit_info, "Service", "X-ReloadIfChanged", false)
531
+
&& !units_to_restart.contains_key(unit)
532
+
&& !(if use_restart_as_stop_and_start {
533
+
units_to_restart.contains_key(unit)
535
+
units_to_stop.contains_key(unit)
538
+
units_to_reload.insert(unit.to_string(), ());
539
+
record_unit(RELOAD_LIST_FILE, unit);
540
+
} else if !parse_systemd_bool(new_unit_info, "Service", "X-RestartIfChanged", true)
541
+
|| parse_systemd_bool(new_unit_info, "Unit", "RefuseManualStop", false)
542
+
|| parse_systemd_bool(new_unit_info, "Unit", "X-OnlyManualStart", false)
544
+
units_to_skip.insert(unit.to_string(), ());
546
+
// It doesn't make sense to stop and start non-services because they can't have
548
+
if !parse_systemd_bool(new_unit_info, "Service", "X-StopIfChanged", true)
549
+
|| !unit.ends_with(".service")
551
+
// This unit should be restarted instead of stopped and started.
552
+
units_to_restart.insert(unit.to_string(), ());
553
+
record_unit(RESTART_LIST_FILE, unit);
554
+
// Remove from units to reload so we don't restart and reload
555
+
if units_to_reload.contains_key(unit) {
556
+
units_to_reload.remove(unit);
557
+
unrecord_unit(RELOAD_LIST_FILE, unit);
560
+
// If this unit is socket-activated, then stop the socket unit(s) as well, and
561
+
// restart the socket(s) instead of the service.
562
+
let mut socket_activated = false;
563
+
if unit.ends_with(".service") {
564
+
let mut sockets = if let Some(Some(Some(sockets))) = new_unit_info.map(|info| {
565
+
info.get("Service")
566
+
.map(|service_section| service_section.get("Sockets"))
570
+
.split_whitespace()
578
+
if sockets.is_empty() {
579
+
sockets.push(format!("{}.socket", base_name));
582
+
for socket in &sockets {
583
+
if active_cur.contains_key(socket) {
584
+
// We can now be sure this is a socket-activated unit
586
+
if use_restart_as_stop_and_start {
587
+
units_to_restart.insert(socket.to_string(), ());
589
+
units_to_stop.insert(socket.to_string(), ());
592
+
// Only restart sockets that actually exist in new configuration:
593
+
if toplevel.join("etc/systemd/system").join(socket).exists() {
594
+
if use_restart_as_stop_and_start {
595
+
units_to_restart.insert(socket.to_string(), ());
596
+
record_unit(RESTART_LIST_FILE, socket);
598
+
units_to_start.insert(socket.to_string(), ());
599
+
record_unit(START_LIST_FILE, socket);
602
+
socket_activated = true;
605
+
// Remove from units to reload so we don't restart and reload
606
+
if units_to_reload.contains_key(unit) {
607
+
units_to_reload.remove(unit);
608
+
unrecord_unit(RELOAD_LIST_FILE, unit);
614
+
// If the unit is not socket-activated, record that this unit needs to be started
615
+
// below. We write this to a file to ensure that the service gets restarted if
616
+
// we're interrupted.
617
+
if !socket_activated {
618
+
if use_restart_as_stop_and_start {
619
+
units_to_restart.insert(unit.to_string(), ());
620
+
record_unit(RESTART_LIST_FILE, unit);
622
+
units_to_start.insert(unit.to_string(), ());
623
+
record_unit(START_LIST_FILE, unit);
627
+
if use_restart_as_stop_and_start {
628
+
units_to_restart.insert(unit.to_string(), ());
630
+
units_to_stop.insert(unit.to_string(), ());
632
+
// Remove from units to reload so we don't restart and reload
633
+
if units_to_reload.contains_key(unit) {
634
+
units_to_reload.remove(unit);
635
+
unrecord_unit(RELOAD_LIST_FILE, unit);
644
+
// Writes a unit name into a given file to be more resilient against crashes of the script. Does
645
+
// nothing when the action is dry-activate.
646
+
fn record_unit(p: impl AsRef<Path>, unit: &str) {
647
+
if ACTION.get() != Some(&Action::DryActivate) {
648
+
if let Ok(mut f) = std::fs::File::options().append(true).create(true).open(p) {
649
+
_ = writeln!(&mut f, "{unit}");
654
+
// The opposite of record_unit, removes a unit name from a file
655
+
fn unrecord_unit(p: impl AsRef<Path>, unit: &str) {
656
+
if ACTION.get() != Some(&Action::DryActivate) {
657
+
if let Ok(contents) = std::fs::read_to_string(&p) {
658
+
if let Ok(mut f) = std::fs::File::options()
667
+
.filter(|line| line != &unit)
668
+
.for_each(|line| _ = writeln!(&mut f, "{line}"))
674
+
fn map_from_list_file(p: impl AsRef<Path>) -> HashMap<String, ()> {
675
+
std::fs::read_to_string(p)
676
+
.unwrap_or_default()
678
+
.filter(|line| !line.is_empty())
680
+
.fold(HashMap::new(), |mut acc, line| {
681
+
acc.insert(line.to_string(), ());
687
+
struct Filesystem {
695
+
struct Swap(String);
697
+
// Parse a fstab file, given its path. Returns a tuple of filesystems and swaps.
699
+
// Filesystems is a hash of mountpoint and { device, fsType, options } Swaps is a hash of device
701
+
fn parse_fstab(fstab: impl BufRead) -> (HashMap<String, Filesystem>, HashMap<String, Swap>) {
702
+
let mut filesystems = HashMap::new();
703
+
let mut swaps = HashMap::new();
705
+
for line in fstab.lines() {
706
+
let Ok(line) = line else {
710
+
if line.contains('#') {
714
+
let mut split = line.split_whitespace();
715
+
let (Some(device), Some(mountpoint), Some(fs_type), options) = (
719
+
split.next().unwrap_or_default(),
724
+
if fs_type == "swap" {
725
+
swaps.insert(device.to_string(), Swap(options.to_string()));
727
+
filesystems.insert(
728
+
mountpoint.to_string(),
730
+
device: device.to_string(),
731
+
fs_type: fs_type.to_string(),
732
+
options: options.to_string(),
738
+
(filesystems, swaps)
741
+
// Converts a path to the name of a systemd mount unit that would be responsible for mounting this
743
+
fn path_to_unit_name(bin_path: &Path, path: &str) -> String {
744
+
let Ok(output) = std::process::Command::new(bin_path.join("systemd-escape"))
745
+
.arg("--suffix=mount")
750
+
eprintln!("Unable to escape {}!", path);
754
+
let Ok(unit) = String::from_utf8(output.stdout) else {
755
+
eprintln!("Unable to convert systemd-espape output to valid UTF-8");
759
+
unit.trim().to_string()
762
+
// Returns a HashMap containing the same contents as the passed in `units`, minus the units in
763
+
// `units_to_filter`.
765
+
units_to_filter: &HashMap<String, ()>,
766
+
units: &HashMap<String, ()>,
767
+
) -> HashMap<String, ()> {
768
+
let mut res = HashMap::new();
770
+
for (unit, _) in units {
771
+
if !units_to_filter.contains_key(unit) {
772
+
res.insert(unit.to_string(), ());
779
+
fn unit_is_active<'a>(conn: &LocalConnection, unit: &str) -> Result<bool> {
780
+
let unit_object_path = conn
782
+
"org.freedesktop.systemd1",
783
+
"/org/freedesktop/systemd1",
784
+
Duration::from_millis(5000),
787
+
.with_context(|| format!("Failed to get unit {unit}"))?;
789
+
let active_state: String = conn
791
+
"org.freedesktop.systemd1",
793
+
Duration::from_millis(5000),
795
+
.get("org.freedesktop.systemd1.Unit", "ActiveState")
796
+
.with_context(|| format!("Failed to get ExecMainStatus for {unit}"))?;
798
+
Ok(matches!(active_state.as_str(), "active" | "activating"))
801
+
static ACTION: OnceLock<Action> = OnceLock::new();
811
+
impl std::fmt::Display for Job {
812
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
817
+
Job::Start => "start",
818
+
Job::Restart => "restart",
819
+
Job::Reload => "reload",
820
+
Job::Stop => "stop",
826
+
fn new_dbus_proxies<'a>(
827
+
conn: &'a LocalConnection,
829
+
Proxy<'a, &'a LocalConnection>,
830
+
Proxy<'a, &'a LocalConnection>,
834
+
"org.freedesktop.systemd1",
835
+
"/org/freedesktop/systemd1",
836
+
Duration::from_millis(5000),
839
+
"org.freedesktop.login1",
840
+
"/org/freedesktop/login1",
841
+
Duration::from_millis(5000),
847
+
conn: &LocalConnection,
848
+
submitted_jobs: &Rc<RefCell<HashMap<dbus::Path<'static>, Job>>>,
850
+
while !submitted_jobs.borrow().is_empty() {
851
+
_ = conn.process(Duration::from_millis(500));
855
+
fn remove_file_if_exists(p: impl AsRef<Path>) -> std::io::Result<()> {
856
+
match std::fs::remove_file(p) {
857
+
Err(err) if err.kind() != std::io::ErrorKind::NotFound => Err(err),
862
+
/// Performs switch-to-configuration functionality for a single non-root user
863
+
fn do_user_switch(parent_exe: String) -> anyhow::Result<()> {
864
+
if Path::new(&parent_exe)
865
+
!= Path::new("/proc/self/exe")
867
+
.context("Failed to get full path to current executable")?
871
+
r#"This program is not meant to be called from outside of switch-to-configuration."#
876
+
let dbus_conn = LocalConnection::new_session().context("Failed to open dbus connection")?;
877
+
let (systemd, _) = new_dbus_proxies(&dbus_conn);
879
+
let nixos_activation_done = Rc::new(RefCell::new(false));
880
+
let _nixos_activation_done = nixos_activation_done.clone();
881
+
let jobs_token = systemd
883
+
move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
884
+
_: &LocalConnection,
886
+
if signal.unit.as_str() == "nixos-activation.service" {
887
+
*_nixos_activation_done.borrow_mut() = true;
893
+
.context("Failed to add signal match for systemd removed jobs")?;
895
+
// The systemd user session seems to not send a Reloaded signal, so we don't have anything to
897
+
_ = systemd.reexecute();
900
+
.restart_unit("nixos-activation.service", "replace")
901
+
.context("Failed to restart nixos-activation.service")?;
903
+
while !*nixos_activation_done.borrow() {
905
+
.process(Duration::from_secs(500))
906
+
.context("Failed to process dbus messages")?;
910
+
.remove_match(jobs_token)
911
+
.context("Failed to remove jobs token")?;
916
+
/// Performs switch-to-configuration functionality for the entire system
917
+
fn do_system_switch() -> anyhow::Result<()> {
918
+
let out = PathBuf::from(required_env("OUT")?);
919
+
let toplevel = PathBuf::from(required_env("TOPLEVEL")?);
920
+
let distro_id = required_env("DISTRO_ID")?;
921
+
let install_bootloader = required_env("INSTALL_BOOTLOADER")?;
922
+
let locale_archive = required_env("LOCALE_ARCHIVE")?;
923
+
let new_systemd = PathBuf::from(required_env("SYSTEMD")?);
925
+
let mut args = std::env::args();
926
+
let argv0 = args.next().ok_or(anyhow!("no argv[0]"))?;
928
+
let Some(Ok(action)) = args.next().map(|a| Action::from_str(&a)) else {
930
+
r#"Usage: {} [switch|boot|test|dry-activate]
931
+
switch: make the configuration the boot default and activate now
932
+
boot: make the configuration the boot default
933
+
test: activate the configuration, but don't make it the boot default
934
+
dry-activate: show what would be done if this configuration were activated
937
+
.split(std::path::MAIN_SEPARATOR_STR)
939
+
.unwrap_or("switch-to-configuration")
941
+
std::process::exit(1);
944
+
let action = ACTION.get_or_init(|| action);
946
+
// The action that is to be performed (like switch, boot, test, dry-activate) Also exposed via
947
+
// environment variable from now on
948
+
std::env::set_var("NIXOS_ACTION", Into::<&'static str>::into(action));
950
+
// Expose the locale archive as an environment variable for systemctl and the activation script
951
+
if !locale_archive.is_empty() {
952
+
std::env::set_var("LOCALE_ARCHIVE", locale_archive);
955
+
let current_system_bin = std::path::PathBuf::from("/run/current-system/sw/bin")
957
+
.context("/run/current-system/sw/bin is missing")?;
959
+
let os_release = parse_os_release().context("Failed to parse os-release")?;
961
+
let distro_id_re = Regex::new(format!("^\"?{}\"?$", distro_id).as_str())
962
+
.context("Invalid regex for distro ID")?;
964
+
// This is a NixOS installation if it has /etc/NIXOS or a proper /etc/os-release.
965
+
if !Path::new("/etc/NIXOS").is_file()
968
+
.map(|id| distro_id_re.is_match(id))
969
+
.unwrap_or_default()
971
+
eprintln!("This is not a NixOS installation!");
975
+
std::fs::create_dir_all("/run/nixos").context("Failed to create /run/nixos directory")?;
976
+
let perms = std::fs::Permissions::from_mode(0o755);
977
+
std::fs::set_permissions("/run/nixos", perms)
978
+
.context("Failed to set permissions on /run/nixos directory")?;
980
+
let Ok(lock) = std::fs::OpenOptions::new()
983
+
.open("/run/nixos/switch-to-configuration.lock")
985
+
eprintln!("Could not open lock");
989
+
let Ok(_lock) = Flock::lock(lock, FlockArg::LockExclusive) else {
990
+
eprintln!("Could not acquire lock");
994
+
if syslog::init(Facility::LOG_USER, LevelFilter::Debug, Some("nixos")).is_err() {
995
+
bail!("Failed to initialize logger");
998
+
// Install or update the bootloader.
999
+
if matches!(action, Action::Switch | Action::Boot) {
1000
+
do_install_bootloader(&install_bootloader, &toplevel)?;
1003
+
// Just in case the new configuration hangs the system, do a sync now.
1004
+
if std::env::var("NIXOS_NO_SYNC")
1006
+
.unwrap_or_default()
1009
+
let fd = nix::fcntl::open("/nix/store", OFlag::O_NOCTTY, Mode::S_IROTH)
1010
+
.context("Failed to open /nix/store")?;
1011
+
nix::unistd::syncfs(fd).context("Failed to sync /nix/store")?;
1014
+
if *action == Action::Boot {
1015
+
std::process::exit(0);
1018
+
let current_init_interface_version =
1019
+
std::fs::read_to_string("/run/current-system/init-interface-version").unwrap_or_default();
1021
+
let new_init_interface_version =
1022
+
std::fs::read_to_string(toplevel.join("init-interface-version"))
1023
+
.context("File init-interface-version should exist")?;
1025
+
// Check if we can activate the new configuration.
1026
+
if current_init_interface_version != new_init_interface_version {
1028
+
r#"Warning: the new NixOS configuration has an ‘init’ that is
1029
+
incompatible with the current configuration. The new configuration
1030
+
won't take effect until you reboot the system.
1033
+
std::process::exit(100);
1036
+
// Ignore SIGHUP so that we're not killed if we're running on (say) virtual console 1 and we
1037
+
// restart the "tty1" unit.
1038
+
let handler = SigHandler::Handler(handle_sigpipe);
1039
+
unsafe { signal::signal(Signal::SIGPIPE, handler) }.context("Failed to set SIGPIPE handler")?;
1041
+
let mut units_to_stop = HashMap::new();
1042
+
let mut units_to_skip = HashMap::new();
1043
+
let mut units_to_filter = HashMap::new(); // units not shown
1045
+
let mut units_to_start = map_from_list_file(START_LIST_FILE);
1046
+
let mut units_to_restart = map_from_list_file(RESTART_LIST_FILE);
1047
+
let mut units_to_reload = map_from_list_file(RELOAD_LIST_FILE);
1049
+
let dbus_conn = LocalConnection::new_system().context("Failed to open dbus connection")?;
1050
+
let (systemd, logind) = new_dbus_proxies(&dbus_conn);
1052
+
let submitted_jobs = Rc::new(RefCell::new(HashMap::new()));
1053
+
let finished_jobs = Rc::new(RefCell::new(HashMap::new()));
1055
+
let systemd_reload_status = Rc::new(RefCell::new(false));
1059
+
.context("Failed to subscribe to systemd dbus messages")?;
1061
+
// Wait for the system to have finished booting.
1063
+
let system_state: String = systemd
1064
+
.get("org.freedesktop.systemd1.Manager", "SystemState")
1065
+
.context("Failed to get system state")?;
1067
+
match system_state.as_str() {
1068
+
"running" | "degraded" | "maintenance" => break,
1071
+
.process(Duration::from_millis(500))
1072
+
.context("Failed to process dbus messages")?
1077
+
let _systemd_reload_status = systemd_reload_status.clone();
1078
+
let reloading_token = systemd
1080
+
move |signal: OrgFreedesktopSystemd1ManagerReloading,
1081
+
_: &LocalConnection,
1083
+
*_systemd_reload_status.borrow_mut() = signal.active;
1088
+
.context("Failed to add systemd Reloading match")?;
1090
+
let _submitted_jobs = submitted_jobs.clone();
1091
+
let _finished_jobs = finished_jobs.clone();
1092
+
let job_removed_token = systemd
1094
+
move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
1095
+
_: &LocalConnection,
1097
+
if let Some(old) = _submitted_jobs.borrow_mut().remove(&signal.job) {
1098
+
let mut finished_jobs = _finished_jobs.borrow_mut();
1099
+
finished_jobs.insert(signal.job, (signal.unit, old, signal.result));
1105
+
.context("Failed to add systemd JobRemoved match")?;
1107
+
let current_active_units = get_active_units(&systemd)?;
1109
+
let template_unit_re = Regex::new(r"^(.*)@[^\.]*\.(.*)$")
1110
+
.context("Invalid regex for matching systemd template units")?;
1111
+
let unit_name_re = Regex::new(r"^(.*)\.[[:lower:]]*$")
1112
+
.context("Invalid regex for matching systemd unit names")?;
1114
+
for (unit, unit_state) in ¤t_active_units {
1115
+
let current_unit_file = Path::new("/etc/systemd/system").join(&unit);
1116
+
let new_unit_file = toplevel.join("etc/systemd/system").join(&unit);
1118
+
let mut base_unit = unit.clone();
1119
+
let mut current_base_unit_file = current_unit_file.clone();
1120
+
let mut new_base_unit_file = new_unit_file.clone();
1122
+
// Detect template instances
1123
+
if let Some((Some(template_name), Some(template_instance))) =
1124
+
template_unit_re.captures(&unit).map(|captures| {
1126
+
captures.get(1).map(|c| c.as_str()),
1127
+
captures.get(2).map(|c| c.as_str()),
1131
+
if !current_unit_file.exists() && !new_unit_file.exists() {
1132
+
base_unit = format!("{}@.{}", template_name, template_instance);
1133
+
current_base_unit_file = Path::new("/etc/systemd/system").join(&base_unit);
1134
+
new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
1138
+
let mut base_name = base_unit.as_str();
1139
+
if let Some(Some(new_base_name)) = unit_name_re
1140
+
.captures(&base_unit)
1141
+
.map(|capture| capture.get(1).map(|first| first.as_str()))
1143
+
base_name = new_base_name;
1146
+
if current_base_unit_file.exists()
1147
+
&& (unit_state.state == "active" || unit_state.state == "activating")
1149
+
if new_base_unit_file
1151
+
.map(|full_path| full_path == Path::new("/dev/null"))
1154
+
let current_unit_info = parse_unit(¤t_unit_file, ¤t_base_unit_file)?;
1155
+
if parse_systemd_bool(Some(¤t_unit_info), "Unit", "X-StopOnRemoval", true) {
1156
+
_ = units_to_stop.insert(unit.to_string(), ());
1158
+
} else if unit.ends_with(".target") {
1159
+
let new_unit_info = parse_unit(&new_unit_file, &new_base_unit_file)?;
1161
+
// Cause all active target units to be restarted below. This should start most
1162
+
// changed units we stop here as well as any new dependencies (including new mounts
1163
+
// and swap devices). FIXME: the suspend target is sometimes active after the
1164
+
// system has resumed, which probably should not be the case. Just ignore it.
1167
+
"suspend.target" | "hibernate.target" | "hybrid-sleep.target"
1169
+
if !(parse_systemd_bool(
1170
+
Some(&new_unit_info),
1172
+
"RefuseManualStart",
1174
+
) || parse_systemd_bool(
1175
+
Some(&new_unit_info),
1177
+
"X-OnlyManualStart",
1180
+
units_to_start.insert(unit.to_string(), ());
1181
+
record_unit(START_LIST_FILE, unit);
1182
+
// Don't spam the user with target units that always get started.
1183
+
if std::env::var("STC_DISPLAY_ALL_UNITS").as_deref() != Ok("1") {
1184
+
units_to_filter.insert(unit.to_string(), ());
1189
+
// Stop targets that have X-StopOnReconfiguration set. This is necessary to respect
1190
+
// dependency orderings involving targets: if unit X starts after target Y and
1191
+
// target Y starts after unit Z, then if X and Z have both changed, then X should
1192
+
// be restarted after Z. However, if target Y is in the "active" state, X and Z
1193
+
// will be restarted at the same time because X's dependency on Y is already
1194
+
// satisfied. Thus, we need to stop Y first. Stopping a target generally has no
1195
+
// effect on other units (unless there is a PartOf dependency), so this is just a
1196
+
// bookkeeping thing to get systemd to do the right thing.
1197
+
if parse_systemd_bool(
1198
+
Some(&new_unit_info),
1200
+
"X-StopOnReconfiguration",
1203
+
units_to_stop.insert(unit.to_string(), ());
1206
+
let current_unit_info = parse_unit(¤t_unit_file, ¤t_base_unit_file)?;
1207
+
let new_unit_info = parse_unit(&new_unit_file, &new_base_unit_file)?;
1208
+
match compare_units(¤t_unit_info, &new_unit_info) {
1209
+
UnitComparison::UnequalNeedsRestart => {
1210
+
handle_modified_unit(
1215
+
&new_base_unit_file,
1216
+
Some(&new_unit_info),
1217
+
¤t_active_units,
1218
+
&mut units_to_stop,
1219
+
&mut units_to_start,
1220
+
&mut units_to_reload,
1221
+
&mut units_to_restart,
1222
+
&mut units_to_skip,
1225
+
UnitComparison::UnequalNeedsReload if !units_to_restart.contains_key(unit) => {
1226
+
units_to_reload.insert(unit.clone(), ());
1227
+
record_unit(RELOAD_LIST_FILE, &unit);
1235
+
// Compare the previous and new fstab to figure out which filesystems need a remount or need to
1236
+
// be unmounted. New filesystems are mounted automatically by starting local-fs.target.
1237
+
// FIXME: might be nicer if we generated units for all mounts; then we could unify this with
1238
+
// the unit checking code above.
1239
+
let (current_filesystems, current_swaps) = std::fs::read_to_string("/etc/fstab")
1240
+
.map(|fstab| parse_fstab(std::io::Cursor::new(fstab)))
1241
+
.unwrap_or_default();
1242
+
let (new_filesystems, new_swaps) = std::fs::read_to_string(toplevel.join("etc/fstab"))
1243
+
.map(|fstab| parse_fstab(std::io::Cursor::new(fstab)))
1244
+
.unwrap_or_default();
1246
+
for (mountpoint, current_filesystem) in current_filesystems {
1247
+
// Use current version of systemctl binary before daemon is reexeced.
1248
+
let unit = path_to_unit_name(¤t_system_bin, &mountpoint);
1249
+
if let Some(new_filesystem) = new_filesystems.get(&mountpoint) {
1250
+
if current_filesystem.fs_type != new_filesystem.fs_type
1251
+
|| current_filesystem.device != new_filesystem.device
1253
+
if matches!(mountpoint.as_str(), "/" | "/nix") {
1254
+
if current_filesystem.options != new_filesystem.options {
1255
+
// Mount options changes, so remount it.
1256
+
units_to_reload.insert(unit.to_string(), ());
1257
+
record_unit(RELOAD_LIST_FILE, &unit)
1259
+
// Don't unmount / or /nix if the device changed
1260
+
units_to_skip.insert(unit, ());
1263
+
// Filesystem type or device changed, so unmount and mount it.
1264
+
units_to_restart.insert(unit.to_string(), ());
1265
+
record_unit(RESTART_LIST_FILE, &unit);
1267
+
} else if current_filesystem.options != new_filesystem.options {
1268
+
// Mount options changes, so remount it.
1269
+
units_to_reload.insert(unit.to_string(), ());
1270
+
record_unit(RELOAD_LIST_FILE, &unit)
1273
+
// Filesystem entry disappeared, so unmount it.
1274
+
units_to_stop.insert(unit, ());
1278
+
// Also handles swap devices.
1279
+
for (device, _) in current_swaps {
1280
+
if new_swaps.get(&device).is_none() {
1281
+
// Swap entry disappeared, so turn it off. Can't use "systemctl stop" here because
1282
+
// systemd has lots of alias units that prevent a stop from actually calling "swapoff".
1283
+
if *action == Action::DryActivate {
1284
+
eprintln!("would stop swap device: {}", &device);
1286
+
eprintln!("stopping swap device: {}", &device);
1287
+
let c_device = std::ffi::CString::new(device.clone())
1288
+
.context("failed to convert device to cstring")?;
1289
+
if unsafe { nix::libc::swapoff(c_device.as_ptr()) } != 0 {
1290
+
let err = std::io::Error::last_os_error();
1291
+
eprintln!("Failed to stop swapping to {device}: {err}");
1295
+
// FIXME: update swap options (i.e. its priority).
1298
+
// Should we have systemd re-exec itself?
1299
+
let current_pid1_path = Path::new("/proc/1/exe")
1301
+
.unwrap_or_else(|_| PathBuf::from("/unknown"));
1302
+
let current_systemd_system_config = Path::new("/etc/systemd/system.conf")
1304
+
.unwrap_or_else(|_| PathBuf::from("/unknown"));
1305
+
let Ok(new_pid1_path) = new_systemd.join("lib/systemd/systemd").canonicalize() else {
1308
+
let new_systemd_system_config = toplevel
1309
+
.join("etc/systemd/system.conf")
1311
+
.unwrap_or_else(|_| PathBuf::from("/unknown"));
1313
+
let restart_systemd = current_pid1_path != new_pid1_path
1314
+
|| current_systemd_system_config != new_systemd_system_config;
1316
+
let units_to_stop_filtered = filter_units(&units_to_filter, &units_to_stop);
1318
+
// Show dry-run actions.
1319
+
if *action == Action::DryActivate {
1320
+
if !units_to_stop_filtered.is_empty() {
1321
+
let mut units = units_to_stop_filtered
1324
+
.map(String::as_str)
1325
+
.collect::<Vec<&str>>();
1326
+
units.sort_by_key(|name| name.to_lowercase());
1327
+
eprintln!("would stop the following units: {}", units.join(", "));
1330
+
if !units_to_skip.is_empty() {
1331
+
let mut units = units_to_skip
1334
+
.map(String::as_str)
1335
+
.collect::<Vec<&str>>();
1336
+
units.sort_by_key(|name| name.to_lowercase());
1338
+
"would NOT stop the following changed units: {}",
1343
+
eprintln!("would activate the configuration...");
1344
+
_ = std::process::Command::new(out.join("dry-activate"))
1347
+
.map(|mut child| child.wait());
1349
+
// Handle the activation script requesting the restart or reload of a unit.
1350
+
for unit in std::fs::read_to_string(DRY_RESTART_BY_ACTIVATION_LIST_FILE)
1351
+
.unwrap_or_default()
1354
+
let current_unit_file = Path::new("/etc/systemd/system").join(unit);
1355
+
let new_unit_file = toplevel.join("etc/systemd/system").join(unit);
1356
+
let mut base_unit = unit.to_string();
1357
+
let mut new_base_unit_file = new_unit_file.clone();
1359
+
// Detect template instances.
1360
+
if let Some((Some(template_name), Some(template_instance))) =
1361
+
template_unit_re.captures(&unit).map(|captures| {
1363
+
captures.get(1).map(|c| c.as_str()),
1364
+
captures.get(2).map(|c| c.as_str()),
1368
+
if !current_unit_file.exists() && !new_unit_file.exists() {
1369
+
base_unit = format!("{}@.{}", template_name, template_instance);
1370
+
new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
1374
+
let mut base_name = base_unit.as_str();
1375
+
if let Some(Some(new_base_name)) = unit_name_re
1376
+
.captures(&base_unit)
1377
+
.map(|capture| capture.get(1).map(|first| first.as_str()))
1379
+
base_name = new_base_name;
1382
+
// Start units if they were not active previously
1383
+
if !current_active_units.contains_key(unit) {
1384
+
units_to_start.insert(unit.to_string(), ());
1388
+
handle_modified_unit(
1393
+
&new_base_unit_file,
1395
+
¤t_active_units,
1396
+
&mut units_to_stop,
1397
+
&mut units_to_start,
1398
+
&mut units_to_reload,
1399
+
&mut units_to_restart,
1400
+
&mut units_to_skip,
1404
+
remove_file_if_exists(DRY_RESTART_BY_ACTIVATION_LIST_FILE)
1405
+
.with_context(|| format!("Failed to remove {}", DRY_RESTART_BY_ACTIVATION_LIST_FILE))?;
1407
+
for unit in std::fs::read_to_string(DRY_RELOAD_BY_ACTIVATION_LIST_FILE)
1408
+
.unwrap_or_default()
1411
+
if current_active_units.contains_key(unit)
1412
+
&& !units_to_restart.contains_key(unit)
1413
+
&& !units_to_stop.contains_key(unit)
1415
+
units_to_reload.insert(unit.to_string(), ());
1416
+
record_unit(RELOAD_LIST_FILE, unit);
1420
+
remove_file_if_exists(DRY_RELOAD_BY_ACTIVATION_LIST_FILE)
1421
+
.with_context(|| format!("Failed to remove {}", DRY_RELOAD_BY_ACTIVATION_LIST_FILE))?;
1423
+
if restart_systemd {
1424
+
eprintln!("would restart systemd");
1427
+
if !units_to_reload.is_empty() {
1428
+
let mut units = units_to_reload
1431
+
.map(String::as_str)
1432
+
.collect::<Vec<&str>>();
1433
+
units.sort_by_key(|name| name.to_lowercase());
1434
+
eprintln!("would reload the following units: {}", units.join(", "));
1437
+
if !units_to_restart.is_empty() {
1438
+
let mut units = units_to_restart
1441
+
.map(String::as_str)
1442
+
.collect::<Vec<&str>>();
1443
+
units.sort_by_key(|name| name.to_lowercase());
1444
+
eprintln!("would restart the following units: {}", units.join(", "));
1447
+
let units_to_start_filtered = filter_units(&units_to_filter, &units_to_start);
1448
+
if !units_to_start_filtered.is_empty() {
1449
+
let mut units = units_to_start_filtered
1452
+
.map(String::as_str)
1453
+
.collect::<Vec<&str>>();
1454
+
units.sort_by_key(|name| name.to_lowercase());
1455
+
eprintln!("would start the following units: {}", units.join(", "));
1458
+
std::process::exit(0);
1461
+
log::info!("switching to system configuration {}", toplevel.display());
1463
+
if !units_to_stop.is_empty() {
1464
+
if !units_to_stop_filtered.is_empty() {
1465
+
let mut units = units_to_stop_filtered
1468
+
.map(String::as_str)
1469
+
.collect::<Vec<&str>>();
1470
+
units.sort_by_key(|name| name.to_lowercase());
1471
+
eprintln!("stopping the following units: {}", units.join(", "));
1474
+
for unit in units_to_stop.keys() {
1475
+
match systemd.stop_unit(unit, "replace") {
1477
+
let mut j = submitted_jobs.borrow_mut();
1478
+
j.insert(job_path.to_owned(), Job::Stop);
1484
+
block_on_jobs(&dbus_conn, &submitted_jobs);
1487
+
if !units_to_skip.is_empty() {
1488
+
let mut units = units_to_skip
1491
+
.map(String::as_str)
1492
+
.collect::<Vec<&str>>();
1493
+
units.sort_by_key(|name| name.to_lowercase());
1495
+
"NOT restarting the following changed units: {}",
1500
+
// Wait for all stop jobs to finish
1501
+
block_on_jobs(&dbus_conn, &submitted_jobs);
1503
+
let mut exit_code = 0;
1505
+
// Activate the new configuration (i.e., update /etc, make accounts, and so on).
1506
+
eprintln!("activating the configuration...");
1507
+
match std::process::Command::new(out.join("activate"))
1510
+
.map(|mut child| child.wait())
1512
+
Ok(Ok(status)) if status.success() => {}
1514
+
// allow toplevel to not have an activation script
1517
+
eprintln!("Failed to run activate script");
1522
+
// Handle the activation script requesting the restart or reload of a unit.
1523
+
for unit in std::fs::read_to_string(RESTART_BY_ACTIVATION_LIST_FILE)
1524
+
.unwrap_or_default()
1527
+
let new_unit_file = toplevel.join("etc/systemd/system").join(unit);
1528
+
let mut base_unit = unit.to_string();
1529
+
let mut new_base_unit_file = new_unit_file.clone();
1531
+
// Detect template instances.
1532
+
if let Some((Some(template_name), Some(template_instance))) =
1533
+
template_unit_re.captures(&unit).map(|captures| {
1535
+
captures.get(1).map(|c| c.as_str()),
1536
+
captures.get(2).map(|c| c.as_str()),
1540
+
if !new_unit_file.exists() {
1541
+
base_unit = format!("{}@.{}", template_name, template_instance);
1542
+
new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
1546
+
let mut base_name = base_unit.as_str();
1547
+
if let Some(Some(new_base_name)) = unit_name_re
1548
+
.captures(&base_unit)
1549
+
.map(|capture| capture.get(1).map(|first| first.as_str()))
1551
+
base_name = new_base_name;
1554
+
// Start units if they were not active previously
1555
+
if !current_active_units.contains_key(unit) {
1556
+
units_to_start.insert(unit.to_string(), ());
1557
+
record_unit(START_LIST_FILE, unit);
1561
+
handle_modified_unit(
1566
+
&new_base_unit_file,
1568
+
¤t_active_units,
1569
+
&mut units_to_stop,
1570
+
&mut units_to_start,
1571
+
&mut units_to_reload,
1572
+
&mut units_to_restart,
1573
+
&mut units_to_skip,
1577
+
// We can remove the file now because it has been propagated to the other restart/reload files
1578
+
remove_file_if_exists(RESTART_BY_ACTIVATION_LIST_FILE)
1579
+
.with_context(|| format!("Failed to remove {}", RESTART_BY_ACTIVATION_LIST_FILE))?;
1581
+
for unit in std::fs::read_to_string(RELOAD_BY_ACTIVATION_LIST_FILE)
1582
+
.unwrap_or_default()
1585
+
if current_active_units.contains_key(unit)
1586
+
&& !units_to_restart.contains_key(unit)
1587
+
&& !units_to_stop.contains_key(unit)
1589
+
units_to_reload.insert(unit.to_string(), ());
1590
+
record_unit(RELOAD_LIST_FILE, unit);
1594
+
// We can remove the file now because it has been propagated to the other reload file
1595
+
remove_file_if_exists(RELOAD_BY_ACTIVATION_LIST_FILE)
1596
+
.with_context(|| format!("Failed to remove {}", RELOAD_BY_ACTIVATION_LIST_FILE))?;
1598
+
// Restart systemd if necessary. Note that this is done using the current version of systemd,
1599
+
// just in case the new one has trouble communicating with the running pid 1.
1600
+
if restart_systemd {
1601
+
eprintln!("restarting systemd...");
1602
+
_ = systemd.reexecute(); // we don't get a dbus reply here
1604
+
while !*systemd_reload_status.borrow() {
1606
+
.process(Duration::from_millis(500))
1607
+
.context("Failed to process dbus messages")?;
1611
+
// Forget about previously failed services.
1614
+
.context("Failed to reset failed units")?;
1616
+
// Make systemd reload its units.
1617
+
_ = systemd.reload(); // we don't get a dbus reply here
1618
+
while !*systemd_reload_status.borrow() {
1620
+
.process(Duration::from_millis(500))
1621
+
.context("Failed to process dbus messages")?;
1625
+
.remove_match(reloading_token)
1626
+
.context("Failed to cleanup systemd Reloading match")?;
1628
+
// Reload user units
1629
+
match logind.list_users() {
1631
+
eprintln!("Unable to list users with logind: {err}");
1635
+
for (uid, name, _) in users {
1636
+
eprintln!("reloading user units for {}...", name);
1637
+
let myself = Path::new("/proc/self/exe")
1639
+
.context("Failed to get full path to /proc/self/exe")?;
1641
+
std::process::Command::new(&myself)
1643
+
.env("XDG_RUNTIME_DIR", format!("/run/user/{}", uid))
1644
+
.env("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE", &myself)
1646
+
.map(|mut child| _ = child.wait())
1647
+
.with_context(|| format!("Failed to run user activation for {name}"))?;
1652
+
// Restart sysinit-reactivation.target. This target only exists to restart services ordered
1653
+
// before sysinit.target. We cannot use X-StopOnReconfiguration to restart sysinit.target
1654
+
// because then ALL services of the system would be restarted since all normal services have a
1655
+
// default dependency on sysinit.target. sysinit-reactivation.target ensures that services
1656
+
// ordered BEFORE sysinit.target get re-started in the correct order. Ordering between these
1657
+
// services is respected.
1658
+
eprintln!("restarting {SYSINIT_REACTIVATION_TARGET}");
1659
+
match systemd.restart_unit(SYSINIT_REACTIVATION_TARGET, "replace") {
1661
+
let mut jobs = submitted_jobs.borrow_mut();
1662
+
jobs.insert(job_path, Job::Restart);
1665
+
eprintln!("Failed to restart {SYSINIT_REACTIVATION_TARGET}: {err}");
1670
+
// Wait for the restart job of sysinit-reactivation.service to finish
1671
+
block_on_jobs(&dbus_conn, &submitted_jobs);
1673
+
// Before reloading we need to ensure that the units are still active. They may have been
1674
+
// deactivated because one of their requirements got stopped. If they are inactive but should
1675
+
// have been reloaded, the user probably expects them to be started.
1676
+
if !units_to_reload.is_empty() {
1677
+
for (unit, _) in units_to_reload.clone() {
1678
+
if !unit_is_active(&dbus_conn, &unit)? {
1679
+
// Figure out if we need to start the unit
1680
+
let unit_info = parse_unit(
1681
+
toplevel.join("etc/systemd/system").join(&unit).as_path(),
1682
+
toplevel.join("etc/systemd/system").join(&unit).as_path(),
1684
+
if !parse_systemd_bool(Some(&unit_info), "Unit", "RefuseManualStart", false)
1685
+
|| parse_systemd_bool(Some(&unit_info), "Unit", "X-OnlyManualStart", false)
1687
+
units_to_start.insert(unit.clone(), ());
1688
+
record_unit(START_LIST_FILE, &unit);
1690
+
// Don't reload the unit, reloading would fail
1691
+
units_to_reload.remove(&unit);
1692
+
unrecord_unit(RELOAD_LIST_FILE, &unit);
1697
+
// Reload units that need it. This includes remounting changed mount units.
1698
+
if !units_to_reload.is_empty() {
1699
+
let mut units = units_to_reload
1702
+
.map(String::as_str)
1703
+
.collect::<Vec<&str>>();
1704
+
units.sort_by_key(|name| name.to_lowercase());
1705
+
eprintln!("reloading the following units: {}", units.join(", "));
1707
+
for unit in units {
1708
+
match systemd.reload_unit(unit, "replace") {
1712
+
.insert(job_path.clone(), Job::Reload);
1715
+
eprintln!("Failed to reload {unit}: {err}");
1721
+
block_on_jobs(&dbus_conn, &submitted_jobs);
1723
+
remove_file_if_exists(RELOAD_LIST_FILE)
1724
+
.with_context(|| format!("Failed to remove {}", RELOAD_LIST_FILE))?;
1727
+
// Restart changed services (those that have to be restarted rather than stopped and started).
1728
+
if !units_to_restart.is_empty() {
1729
+
let mut units = units_to_restart
1732
+
.map(String::as_str)
1733
+
.collect::<Vec<&str>>();
1734
+
units.sort_by_key(|name| name.to_lowercase());
1735
+
eprintln!("restarting the following units: {}", units.join(", "));
1737
+
for unit in units {
1738
+
match systemd.restart_unit(unit, "replace") {
1740
+
let mut jobs = submitted_jobs.borrow_mut();
1741
+
jobs.insert(job_path, Job::Restart);
1744
+
eprintln!("Failed to restart {unit}: {err}");
1750
+
block_on_jobs(&dbus_conn, &submitted_jobs);
1752
+
remove_file_if_exists(RESTART_LIST_FILE)
1753
+
.with_context(|| format!("Failed to remove {}", RESTART_LIST_FILE))?;
1756
+
// Start all active targets, as well as changed units we stopped above. The latter is necessary
1757
+
// because some may not be dependencies of the targets (i.e., they were manually started).
1758
+
// FIXME: detect units that are symlinks to other units. We shouldn't start both at the same
1759
+
// time because we'll get a "Failed to add path to set" error from systemd.
1760
+
let units_to_start_filtered = filter_units(&units_to_filter, &units_to_start);
1761
+
if !units_to_start_filtered.is_empty() {
1762
+
let mut units = units_to_start_filtered
1765
+
.map(String::as_str)
1766
+
.collect::<Vec<&str>>();
1767
+
units.sort_by_key(|name| name.to_lowercase());
1768
+
eprintln!("starting the following units: {}", units.join(", "));
1771
+
for unit in units_to_start.keys() {
1772
+
match systemd.start_unit(unit, "replace") {
1774
+
let mut jobs = submitted_jobs.borrow_mut();
1775
+
jobs.insert(job_path, Job::Start);
1778
+
eprintln!("Failed to start {unit}: {err}");
1784
+
block_on_jobs(&dbus_conn, &submitted_jobs);
1786
+
remove_file_if_exists(START_LIST_FILE)
1787
+
.with_context(|| format!("Failed to remove {}", START_LIST_FILE))?;
1789
+
for (unit, job, result) in finished_jobs.borrow().values() {
1790
+
match result.as_str() {
1791
+
"timeout" | "failed" | "dependency" => {
1792
+
eprintln!("Failed to {} {}", job, unit);
1800
+
.remove_match(job_removed_token)
1801
+
.context("Failed to cleanup systemd job match")?;
1803
+
// Print failed and new units.
1804
+
let mut failed_units = Vec::new();
1805
+
let mut new_units = Vec::new();
1807
+
// NOTE: We want switch-to-configuration to be able to report to the user any units that failed
1808
+
// to start or units that systemd had to restart due to having previously failed. This is
1809
+
// inherently a race condition between how long our program takes to run and how long the unit
1810
+
// in question takes to potentially fail. The amount of time we wait for new messages on the
1811
+
// bus to settle is purely tuned so that this program is compatible with the Perl
1812
+
// implementation.
1814
+
// Wait for events from systemd to settle. process() will return true if we have received any
1815
+
// messages on the bus.
1817
+
.process(Duration::from_millis(250))
1818
+
.unwrap_or_default()
1821
+
let new_active_units = get_active_units(&systemd)?;
1823
+
for (unit, unit_state) in new_active_units {
1824
+
if &unit_state.state == "failed" {
1825
+
failed_units.push(unit);
1829
+
if unit_state.substate == "auto-restart" && unit.ends_with(".service") {
1830
+
// A unit in auto-restart substate is a failure *if* it previously failed to start
1831
+
let unit_object_path = systemd
1833
+
.context("Failed to get unit info for {unit}")?;
1834
+
let exec_main_status: i32 = dbus_conn
1836
+
"org.freedesktop.systemd1",
1838
+
Duration::from_millis(5000),
1840
+
.get("org.freedesktop.systemd1.Service", "ExecMainStatus")
1841
+
.context("Failed to get ExecMainStatus for {unit}")?;
1843
+
if exec_main_status != 0 {
1844
+
failed_units.push(unit);
1849
+
// Ignore scopes since they are not managed by this script but rather created and managed
1850
+
// by third-party services via the systemd dbus API. This only lists units that are not
1851
+
// failed (including ones that are in auto-restart but have not failed previously)
1852
+
if unit_state.state != "failed"
1853
+
&& !current_active_units.contains_key(&unit)
1854
+
&& !unit.ends_with(".scope")
1856
+
new_units.push(unit);
1860
+
if !new_units.is_empty() {
1861
+
new_units.sort_by_key(|name| name.to_lowercase());
1863
+
"the following new units were started: {}",
1864
+
new_units.join(", ")
1868
+
if !failed_units.is_empty() {
1869
+
failed_units.sort_by_key(|name| name.to_lowercase());
1871
+
"warning: the following units failed: {}",
1872
+
failed_units.join(", ")
1874
+
_ = std::process::Command::new(new_systemd.join("bin/systemctl"))
1876
+
.arg("--no-pager")
1878
+
.args(failed_units)
1880
+
.map(|mut child| child.wait());
1885
+
if exit_code == 0 {
1887
+
"finished switching to system configuration {}",
1888
+
toplevel.display()
1892
+
"switching to system configuration {} failed (status {})",
1893
+
toplevel.display(),
1898
+
std::process::exit(exit_code);
1901
+
fn main() -> anyhow::Result<()> {
1903
+
unsafe { nix::libc::geteuid() },
1904
+
std::env::var("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE").ok(),
1906
+
(0, None) => do_system_switch(),
1907
+
(1..=u32::MAX, None) => bail!("This program does not support being ran outside of the switch-to-configuration environment"),
1908
+
(_, Some(parent_exe)) => do_user_switch(parent_exe),
1914
+
use std::collections::HashMap;
1917
+
fn parse_fstab() {
1919
+
let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(""));
1920
+
assert!(filesystems.is_empty());
1921
+
assert!(swaps.is_empty());
1925
+
let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
1930
+
assert!(filesystems.is_empty());
1931
+
assert!(swaps.is_empty());
1935
+
let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
1937
+
# This is a generated file. Do not edit!
1939
+
# To make changes, edit the fileSystems and swapDevices NixOS options
1940
+
# in your /etc/nixos/configuration.nix file.
1942
+
# <file system> <mount point> <type> <options> <dump> <pass>
1945
+
/dev/mapper/root / btrfs x-initrd.mount,compress=zstd,noatime,defaults 0 0
1946
+
/dev/disk/by-partlabel/BOOT /boot vfat x-systemd.automount 0 2
1947
+
/dev/disk/by-partlabel/home /home ext4 defaults 0 2
1948
+
/dev/mapper/usr /nix/.ro-store erofs x-initrd.mount,ro 0 2
1954
+
assert_eq!(filesystems.len(), 4);
1955
+
assert_eq!(swaps.len(), 0);
1956
+
let home_fs = filesystems.get("/home").unwrap();
1957
+
assert_eq!(home_fs.fs_type, "ext4");
1958
+
assert_eq!(home_fs.device, "/dev/disk/by-partlabel/home");
1959
+
assert_eq!(home_fs.options, "defaults");
1964
+
fn filter_units() {
1966
+
super::filter_units(&HashMap::from([]), &HashMap::from([])),
1971
+
super::filter_units(
1972
+
&HashMap::from([("foo".to_string(), ())]),
1973
+
&HashMap::from([("foo".to_string(), ()), ("bar".to_string(), ())])
1975
+
HashMap::from([("bar".to_string(), ())])
1980
+
fn compare_units() {
1983
+
super::compare_units(&HashMap::from([]), &HashMap::from([]))
1984
+
== super::UnitComparison::Equal
1988
+
super::compare_units(
1989
+
&HashMap::from([("Unit".to_string(), HashMap::from([]))]),
1990
+
&HashMap::from([])
1991
+
) == super::UnitComparison::Equal
1995
+
super::compare_units(
1997
+
"Unit".to_string(),
1999
+
"X-Reload-Triggers".to_string(),
2000
+
vec!["foobar".to_string()]
2003
+
&HashMap::from([])
2004
+
) == super::UnitComparison::Equal
2010
+
super::compare_units(
2011
+
&HashMap::from([("foobar".to_string(), HashMap::from([]))]),
2012
+
&HashMap::from([])
2013
+
) == super::UnitComparison::UnequalNeedsRestart
2017
+
super::compare_units(
2019
+
"Mount".to_string(),
2020
+
HashMap::from([("Options".to_string(), vec![])])
2023
+
"Mount".to_string(),
2024
+
HashMap::from([("Options".to_string(), vec!["ro".to_string()])])
2026
+
) == super::UnitComparison::UnequalNeedsReload
2032
+
super::compare_units(
2033
+
&HashMap::from([]),
2035
+
"Unit".to_string(),
2037
+
"X-Reload-Triggers".to_string(),
2038
+
vec!["foobar".to_string()]
2041
+
) == super::UnitComparison::UnequalNeedsReload
2045
+
super::compare_units(
2047
+
"Unit".to_string(),
2049
+
"X-Reload-Triggers".to_string(),
2050
+
vec!["foobar".to_string()]
2054
+
"Unit".to_string(),
2056
+
"X-Reload-Triggers".to_string(),
2057
+
vec!["barfoo".to_string()]
2060
+
) == super::UnitComparison::UnequalNeedsReload
2064
+
super::compare_units(
2066
+
"Mount".to_string(),
2067
+
HashMap::from([("Type".to_string(), vec!["ext4".to_string()])])
2070
+
"Mount".to_string(),
2071
+
HashMap::from([("Type".to_string(), vec!["btrfs".to_string()])])
2073
+
) == super::UnitComparison::UnequalNeedsRestart