Merge pull request #329823 from ExpidusOS/fix/pkgsllvm/elfutils
[NixPkgs.git] / pkgs / by-name / sw / switch-to-configuration-ng / src / main.rs
blob057604b0317810c05f50f6125406273ceb6a9fde
1 use std::{
2     cell::RefCell,
3     collections::HashMap,
4     io::{BufRead, Read, Write},
5     os::unix::{fs::PermissionsExt, process::CommandExt},
6     path::{Path, PathBuf},
7     rc::Rc,
8     str::FromStr,
9     sync::OnceLock,
10     time::Duration,
13 use anyhow::{anyhow, bail, Context, Result};
14 use dbus::{
15     blocking::{stdintf::org_freedesktop_dbus::Properties, LocalConnection, Proxy},
16     Message,
18 use glob::glob;
19 use ini::{Ini, ParseOption};
20 use log::LevelFilter;
21 use nix::{
22     fcntl::{Flock, FlockArg, OFlag},
23     sys::{
24         signal::{self, SigHandler, Signal},
25         stat::Mode,
26     },
28 use regex::Regex;
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)]
35     #![allow(unused)]
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)]
43     #![allow(unused)]
44     include!(concat!(env!("OUT_DIR"), "/logind_manager.rs"));
47 use crate::systemd_manager::OrgFreedesktopSystemd1Manager;
48 use crate::{
49     logind_manager::OrgFreedesktopLogin1Manager,
50     systemd_manager::{
51         OrgFreedesktopSystemd1ManagerJobRemoved, OrgFreedesktopSystemd1ManagerReloading,
52     },
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
61 // left off.
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
70 // NixOS module.
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)]
80 enum Action {
81     Switch,
82     Boot,
83     Test,
84     DryActivate,
87 impl std::str::FromStr for Action {
88     type Err = anyhow::Error;
90     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
91         Ok(match s {
92             "switch" => Self::Switch,
93             "boot" => Self::Boot,
94             "test" => Self::Test,
95             "dry-activate" => Self::DryActivate,
96             _ => bail!("invalid action {s}"),
97         })
98     }
101 impl Into<&'static str> for &Action {
102     fn into(self) -> &'static str {
103         match self {
104             Action::Switch => "switch",
105             Action::Boot => "boot",
106             Action::Test => "test",
107             Action::DryActivate => "dry-activate",
108         }
109     }
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
114 fn die() -> ! {
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")?
121         .lines()
122         .into_iter()
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());
126             }
128             acc
129         }))
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");
136     };
138     match std::process::Command::new(argv0)
139         .args(cmd_split.collect::<Vec<&str>>())
140         .arg(toplevel)
141         .spawn()
142         .map(|mut child| child.wait())
143     {
144         Ok(Ok(status)) if status.success() => {}
145         _ => {
146             eprintln!("Failed to install bootloader");
147             die();
148         }
149     }
151     Ok(())
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}"))
160 #[derive(Debug)]
161 struct UnitState {
162     state: String,
163     substate: String,
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")?;
175     Ok(units
176         .into_iter()
177         .filter_map(
178             |(
179                 id,
180                 _description,
181                 _load_state,
182                 active_state,
183                 sub_state,
184                 following,
185                 _unit_path,
186                 _job_id,
187                 _job_type,
188                 _job_path,
189             )| {
190                 if following == "" && active_state != "inactive" {
191                     Some((id, active_state, sub_state))
192                 } else {
193                     None
194                 }
195             },
196         )
197         .fold(HashMap::new(), |mut acc, (id, active_state, sub_state)| {
198             acc.insert(
199                 id,
200                 UnitState {
201                     state: active_state,
202                     substate: sub_state,
203                 },
204             );
206             acc
207         }))
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
218 // override files.
219 fn parse_systemd_ini(data: &mut UnitInfo, mut unit_file: impl Read) -> Result<()> {
220     let mut unit_file_content = String::new();
221     _ = unit_file
222         .read_to_string(&mut unit_file_content)
223         .context("Failed to read unit file")?;
225     let ini = Ini::load_from_str_opt(
226         &unit_file_content,
227         ParseOption {
228             enabled_quote: true,
229             // Allow for escaped characters that won't get interpreted by the INI parser. These
230             // often show up in systemd unit files device/mount/swap unit names (e.g. dev-disk-by\x2dlabel-root.device).
231             enabled_escape: false,
232         },
233     )
234     .context("Failed parse unit file as INI")?;
236     // Copy over all sections
237     for (section, properties) in ini.iter() {
238         let Some(section) = section else {
239             continue;
240         };
242         if section == "Install" {
243             // Skip the [Install] section because it has no relevant keys for us
244             continue;
245         }
247         let section_map = if let Some(section_map) = data.get_mut(section) {
248             section_map
249         } else {
250             data.insert(section.to_string(), HashMap::new());
251             data.get_mut(section)
252                 .ok_or(anyhow!("section name should exist in hashmap"))?
253         };
255         for (ini_key, _) in properties {
256             let values = properties.get_all(ini_key);
257             let values = values
258                 .into_iter()
259                 .map(String::from)
260                 .collect::<Vec<String>>();
262             // Go over all values
263             let mut new_vals = Vec::new();
264             let mut clear_existing = false;
266             for val in values {
267                 // If a value is empty, it's an override that tells us to clean the value
268                 if val.is_empty() {
269                     new_vals.clear();
270                     clear_existing = true;
271                 } else {
272                     new_vals.push(val);
273                 }
274             }
276             match (section_map.get_mut(ini_key), clear_existing) {
277                 (Some(existing_vals), false) => existing_vals.extend(new_vals),
278                 _ => _ = section_map.insert(ini_key.to_string(), new_vals),
279             };
280         }
281     }
283     Ok(())
286 // This function takes the path to a systemd configuration file (like a unit configuration) and
287 // parses it into a UnitInfo structure.
289 // If a directory with the same basename ending in .d exists next to the unit file, it will be
290 // assumed to contain override files which will be parsed as well and handled properly.
291 fn parse_unit(unit_file: &Path, base_unit_path: &Path) -> Result<UnitInfo> {
292     // Parse the main unit and all overrides
293     let mut unit_data = HashMap::new();
295     let base_unit_file = std::fs::File::open(base_unit_path)
296         .with_context(|| format!("Failed to open unit file {}", base_unit_path.display()))?;
297     parse_systemd_ini(&mut unit_data, base_unit_file).with_context(|| {
298         format!(
299             "Failed to parse systemd unit file {}",
300             base_unit_path.display()
301         )
302     })?;
304     for entry in
305         glob(&format!("{}.d/*.conf", base_unit_path.display())).context("Invalid glob pattern")?
306     {
307         let Ok(entry) = entry else {
308             continue;
309         };
311         let unit_file = std::fs::File::open(&entry)
312             .with_context(|| format!("Failed to open unit file {}", entry.display()))?;
313         parse_systemd_ini(&mut unit_data, unit_file)?;
314     }
316     // Handle drop-in template-unit instance overrides
317     if unit_file != base_unit_path {
318         for entry in
319             glob(&format!("{}.d/*.conf", unit_file.display())).context("Invalid glob pattern")?
320         {
321             let Ok(entry) = entry else {
322                 continue;
323             };
325             let unit_file = std::fs::File::open(&entry)
326                 .with_context(|| format!("Failed to open unit file {}", entry.display()))?;
327             parse_systemd_ini(&mut unit_data, unit_file)?;
328         }
329     }
331     Ok(unit_data)
334 // Checks whether a specified boolean in a systemd unit is true or false, with a default that is
335 // applied when the value is not set.
336 fn parse_systemd_bool(
337     unit_data: Option<&UnitInfo>,
338     section_name: &str,
339     bool_name: &str,
340     default: bool,
341 ) -> bool {
342     if let Some(Some(Some(Some(b)))) = unit_data.map(|data| {
343         data.get(section_name).map(|section| {
344             section.get(bool_name).map(|vals| {
345                 vals.last()
346                     .map(|last| matches!(last.as_str(), "1" | "yes" | "true" | "on"))
347             })
348         })
349     }) {
350         b
351     } else {
352         default
353     }
356 #[derive(Debug, PartialEq)]
357 enum UnitComparison {
358     Equal,
359     UnequalNeedsRestart,
360     UnequalNeedsReload,
363 // Compare the contents of two unit files and return whether the unit needs to be restarted or
364 // reloaded. If the units differ, the service is restarted unless the only difference is
365 // `X-Reload-Triggers` in the `Unit` section. If this is the only modification, the unit is
366 // reloaded instead of restarted. If the only difference is `Options` in the `[Mount]` section, the
367 // unit is reloaded rather than restarted.
368 fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison {
369     let mut ret = UnitComparison::Equal;
371     let unit_section_ignores = HashMap::from(
372         [
373             "X-Reload-Triggers",
374             "Description",
375             "Documentation",
376             "OnFailure",
377             "OnSuccess",
378             "OnFailureJobMode",
379             "IgnoreOnIsolate",
380             "StopWhenUnneeded",
381             "RefuseManualStart",
382             "RefuseManualStop",
383             "AllowIsolate",
384             "CollectMode",
385             "SourcePath",
386         ]
387         .map(|name| (name, ())),
388     );
390     let mut section_cmp = new_unit.keys().fold(HashMap::new(), |mut acc, key| {
391         acc.insert(key.as_str(), ());
392         acc
393     });
395     // Iterate over the sections
396     for (section_name, section_val) in current_unit {
397         // Missing section in the new unit?
398         if !section_cmp.contains_key(section_name.as_str()) {
399             // If the [Unit] section was removed, make sure that only keys were in it that are
400             // ignored
401             if section_name == "Unit" {
402                 for (ini_key, _ini_val) in section_val {
403                     if !unit_section_ignores.contains_key(ini_key.as_str()) {
404                         return UnitComparison::UnequalNeedsRestart;
405                     }
406                 }
407                 continue; // check the next section
408             } else {
409                 return UnitComparison::UnequalNeedsRestart;
410             }
411         }
413         section_cmp.remove(section_name.as_str());
415         // Comparison hash for the section contents
416         let mut ini_cmp = new_unit
417             .get(section_name)
418             .map(|section_val| {
419                 section_val.keys().fold(HashMap::new(), |mut acc, ini_key| {
420                     acc.insert(ini_key.as_str(), ());
421                     acc
422                 })
423             })
424             .unwrap_or_default();
426         // Iterate over the keys of the section
427         for (ini_key, current_value) in section_val {
428             ini_cmp.remove(ini_key.as_str());
429             let Some(Some(new_value)) = new_unit
430                 .get(section_name)
431                 .map(|section| section.get(ini_key))
432             else {
433                 // If the key is missing in the new unit, they are different unless the key that is
434                 // now missing is one of the ignored keys
435                 if section_name == "Unit" && unit_section_ignores.contains_key(ini_key.as_str()) {
436                     continue;
437                 }
438                 return UnitComparison::UnequalNeedsRestart;
439             };
441             // If the contents are different, the units are different
442             if current_value != new_value {
443                 if section_name == "Unit" {
444                     if ini_key == "X-Reload-Triggers" {
445                         ret = UnitComparison::UnequalNeedsReload;
446                         continue;
447                     } else if unit_section_ignores.contains_key(ini_key.as_str()) {
448                         continue;
449                     }
450                 }
452                 // If this is a mount unit, check if it was only `Options`
453                 if section_name == "Mount" && ini_key == "Options" {
454                     ret = UnitComparison::UnequalNeedsReload;
455                     continue;
456                 }
458                 return UnitComparison::UnequalNeedsRestart;
459             }
460         }
462         // A key was introduced that was missing in the previous unit
463         if !ini_cmp.is_empty() {
464             if section_name == "Unit" {
465                 for (ini_key, _) in ini_cmp {
466                     if ini_key == "X-Reload-Triggers" {
467                         ret = UnitComparison::UnequalNeedsReload;
468                     } else if unit_section_ignores.contains_key(ini_key) {
469                         continue;
470                     } else {
471                         return UnitComparison::UnequalNeedsRestart;
472                     }
473                 }
474             } else {
475                 return UnitComparison::UnequalNeedsRestart;
476             }
477         }
478     }
480     // A section was introduced that was missing in the previous unit
481     if !section_cmp.is_empty() {
482         if section_cmp.keys().len() == 1 && section_cmp.contains_key("Unit") {
483             if let Some(new_unit_unit) = new_unit.get("Unit") {
484                 for (ini_key, _) in new_unit_unit {
485                     if !unit_section_ignores.contains_key(ini_key.as_str()) {
486                         return UnitComparison::UnequalNeedsRestart;
487                     } else if ini_key == "X-Reload-Triggers" {
488                         ret = UnitComparison::UnequalNeedsReload;
489                     }
490                 }
491             }
492         } else {
493             return UnitComparison::UnequalNeedsRestart;
494         }
495     }
497     ret
500 // Called when a unit exists in both the old systemd and the new system and the units differ. This
501 // figures out of what units are to be stopped, restarted, reloaded, started, and skipped.
502 fn handle_modified_unit(
503     toplevel: &Path,
504     unit: &str,
505     base_name: &str,
506     new_unit_file: &Path,
507     new_base_unit_file: &Path,
508     new_unit_info: Option<&UnitInfo>,
509     active_cur: &HashMap<String, UnitState>,
510     units_to_stop: &mut HashMap<String, ()>,
511     units_to_start: &mut HashMap<String, ()>,
512     units_to_reload: &mut HashMap<String, ()>,
513     units_to_restart: &mut HashMap<String, ()>,
514     units_to_skip: &mut HashMap<String, ()>,
515 ) -> Result<()> {
516     let use_restart_as_stop_and_start = new_unit_info.is_none();
518     if matches!(
519         unit,
520         "sysinit.target" | "basic.target" | "multi-user.target" | "graphical.target"
521     ) || unit.ends_with(".unit")
522         || unit.ends_with(".slice")
523     {
524         // Do nothing.  These cannot be restarted directly.
526         // Slices and Paths don't have to be restarted since properties (resource limits and
527         // inotify watches) seem to get applied on daemon-reload.
528     } else if unit.ends_with(".mount") {
529         // Just restart the unit. We wouldn't have gotten into this subroutine if only `Options`
530         // was changed, in which case the unit would be reloaded. The only exception is / and /nix
531         // because it's very unlikely we can safely unmount them so we reload them instead. This
532         // means that we may not get all changes into the running system but it's better than
533         // crashing it.
534         if unit == "-.mount" || unit == "nix.mount" {
535             units_to_reload.insert(unit.to_string(), ());
536             record_unit(RELOAD_LIST_FILE, unit);
537         } else {
538             units_to_restart.insert(unit.to_string(), ());
539             record_unit(RESTART_LIST_FILE, unit);
540         }
541     } else if unit.ends_with(".socket") {
542         // FIXME: do something?
543         // Attempt to fix this: https://github.com/NixOS/nixpkgs/pull/141192
544         // Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
545         // More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
546     } else {
547         let fallback = parse_unit(new_unit_file, new_base_unit_file)?;
548         let new_unit_info = if new_unit_info.is_some() {
549             new_unit_info
550         } else {
551             Some(&fallback)
552         };
554         if parse_systemd_bool(new_unit_info, "Service", "X-ReloadIfChanged", false)
555             && !units_to_restart.contains_key(unit)
556             && !(if use_restart_as_stop_and_start {
557                 units_to_restart.contains_key(unit)
558             } else {
559                 units_to_stop.contains_key(unit)
560             })
561         {
562             units_to_reload.insert(unit.to_string(), ());
563             record_unit(RELOAD_LIST_FILE, unit);
564         } else if !parse_systemd_bool(new_unit_info, "Service", "X-RestartIfChanged", true)
565             || parse_systemd_bool(new_unit_info, "Unit", "RefuseManualStop", false)
566             || parse_systemd_bool(new_unit_info, "Unit", "X-OnlyManualStart", false)
567         {
568             units_to_skip.insert(unit.to_string(), ());
569         } else {
570             // It doesn't make sense to stop and start non-services because they can't have
571             // ExecStop=
572             if !parse_systemd_bool(new_unit_info, "Service", "X-StopIfChanged", true)
573                 || !unit.ends_with(".service")
574             {
575                 // This unit should be restarted instead of stopped and started.
576                 units_to_restart.insert(unit.to_string(), ());
577                 record_unit(RESTART_LIST_FILE, unit);
578                 // Remove from units to reload so we don't restart and reload
579                 if units_to_reload.contains_key(unit) {
580                     units_to_reload.remove(unit);
581                     unrecord_unit(RELOAD_LIST_FILE, unit);
582                 }
583             } else {
584                 // If this unit is socket-activated, then stop the socket unit(s) as well, and
585                 // restart the socket(s) instead of the service.
586                 let mut socket_activated = false;
587                 if unit.ends_with(".service") {
588                     let mut sockets = if let Some(Some(Some(sockets))) = new_unit_info.map(|info| {
589                         info.get("Service")
590                             .map(|service_section| service_section.get("Sockets"))
591                     }) {
592                         sockets
593                             .join(" ")
594                             .split_whitespace()
595                             .into_iter()
596                             .map(String::from)
597                             .collect()
598                     } else {
599                         Vec::new()
600                     };
602                     if sockets.is_empty() {
603                         sockets.push(format!("{}.socket", base_name));
604                     }
606                     for socket in &sockets {
607                         if active_cur.contains_key(socket) {
608                             // We can now be sure this is a socket-activated unit
610                             if use_restart_as_stop_and_start {
611                                 units_to_restart.insert(socket.to_string(), ());
612                             } else {
613                                 units_to_stop.insert(socket.to_string(), ());
614                             }
616                             // Only restart sockets that actually exist in new configuration:
617                             if toplevel.join("etc/systemd/system").join(socket).exists() {
618                                 if use_restart_as_stop_and_start {
619                                     units_to_restart.insert(socket.to_string(), ());
620                                     record_unit(RESTART_LIST_FILE, socket);
621                                 } else {
622                                     units_to_start.insert(socket.to_string(), ());
623                                     record_unit(START_LIST_FILE, socket);
624                                 }
626                                 socket_activated = true;
627                             }
629                             // Remove from units to reload so we don't restart and reload
630                             if units_to_reload.contains_key(unit) {
631                                 units_to_reload.remove(unit);
632                                 unrecord_unit(RELOAD_LIST_FILE, unit);
633                             }
634                         }
635                     }
636                 }
638                 // If the unit is not socket-activated, record that this unit needs to be started
639                 // below. We write this to a file to ensure that the service gets restarted if
640                 // we're interrupted.
641                 if !socket_activated {
642                     if use_restart_as_stop_and_start {
643                         units_to_restart.insert(unit.to_string(), ());
644                         record_unit(RESTART_LIST_FILE, unit);
645                     } else {
646                         units_to_start.insert(unit.to_string(), ());
647                         record_unit(START_LIST_FILE, unit);
648                     }
649                 }
651                 if use_restart_as_stop_and_start {
652                     units_to_restart.insert(unit.to_string(), ());
653                 } else {
654                     units_to_stop.insert(unit.to_string(), ());
655                 }
656                 // Remove from units to reload so we don't restart and reload
657                 if units_to_reload.contains_key(unit) {
658                     units_to_reload.remove(unit);
659                     unrecord_unit(RELOAD_LIST_FILE, unit);
660                 }
661             }
662         }
663     }
665     Ok(())
668 // Writes a unit name into a given file to be more resilient against crashes of the script. Does
669 // nothing when the action is dry-activate.
670 fn record_unit(p: impl AsRef<Path>, unit: &str) {
671     if ACTION.get() != Some(&Action::DryActivate) {
672         if let Ok(mut f) = std::fs::File::options().append(true).create(true).open(p) {
673             _ = writeln!(&mut f, "{unit}");
674         }
675     }
678 // The opposite of record_unit, removes a unit name from a file
679 fn unrecord_unit(p: impl AsRef<Path>, unit: &str) {
680     if ACTION.get() != Some(&Action::DryActivate) {
681         if let Ok(contents) = std::fs::read_to_string(&p) {
682             if let Ok(mut f) = std::fs::File::options()
683                 .write(true)
684                 .truncate(true)
685                 .create(true)
686                 .open(&p)
687             {
688                 contents
689                     .lines()
690                     .into_iter()
691                     .filter(|line| line != &unit)
692                     .for_each(|line| _ = writeln!(&mut f, "{line}"))
693             }
694         }
695     }
698 fn map_from_list_file(p: impl AsRef<Path>) -> HashMap<String, ()> {
699     std::fs::read_to_string(p)
700         .unwrap_or_default()
701         .lines()
702         .filter(|line| !line.is_empty())
703         .into_iter()
704         .fold(HashMap::new(), |mut acc, line| {
705             acc.insert(line.to_string(), ());
706             acc
707         })
710 #[derive(Debug)]
711 struct Filesystem {
712     device: String,
713     fs_type: String,
714     options: String,
717 #[derive(Debug)]
718 #[allow(unused)]
719 struct Swap(String);
721 // Parse a fstab file, given its path. Returns a tuple of filesystems and swaps.
723 // Filesystems is a hash of mountpoint and { device, fsType, options } Swaps is a hash of device
724 // and { options }
725 fn parse_fstab(fstab: impl BufRead) -> (HashMap<String, Filesystem>, HashMap<String, Swap>) {
726     let mut filesystems = HashMap::new();
727     let mut swaps = HashMap::new();
729     for line in fstab.lines() {
730         let Ok(line) = line else {
731             break;
732         };
734         if line.contains('#') {
735             continue;
736         }
738         let mut split = line.split_whitespace();
739         let (Some(device), Some(mountpoint), Some(fs_type), options) = (
740             split.next(),
741             split.next(),
742             split.next(),
743             split.next().unwrap_or_default(),
744         ) else {
745             continue;
746         };
748         if fs_type == "swap" {
749             swaps.insert(device.to_string(), Swap(options.to_string()));
750         } else {
751             filesystems.insert(
752                 mountpoint.to_string(),
753                 Filesystem {
754                     device: device.to_string(),
755                     fs_type: fs_type.to_string(),
756                     options: options.to_string(),
757                 },
758             );
759         }
760     }
762     (filesystems, swaps)
765 // Converts a path to the name of a systemd mount unit that would be responsible for mounting this
766 // path.
767 fn path_to_unit_name(bin_path: &Path, path: &str) -> String {
768     let Ok(output) = std::process::Command::new(bin_path.join("systemd-escape"))
769         .arg("--suffix=mount")
770         .arg("-p")
771         .arg(path)
772         .output()
773     else {
774         eprintln!("Unable to escape {}!", path);
775         die();
776     };
778     let Ok(unit) = String::from_utf8(output.stdout) else {
779         eprintln!("Unable to convert systemd-espape output to valid UTF-8");
780         die();
781     };
783     unit.trim().to_string()
786 // Returns a HashMap containing the same contents as the passed in `units`, minus the units in
787 // `units_to_filter`.
788 fn filter_units(
789     units_to_filter: &HashMap<String, ()>,
790     units: &HashMap<String, ()>,
791 ) -> HashMap<String, ()> {
792     let mut res = HashMap::new();
794     for (unit, _) in units {
795         if !units_to_filter.contains_key(unit) {
796             res.insert(unit.to_string(), ());
797         }
798     }
800     res
803 fn unit_is_active<'a>(conn: &LocalConnection, unit: &str) -> Result<bool> {
804     let unit_object_path = conn
805         .with_proxy(
806             "org.freedesktop.systemd1",
807             "/org/freedesktop/systemd1",
808             Duration::from_millis(5000),
809         )
810         .get_unit(unit)
811         .with_context(|| format!("Failed to get unit {unit}"))?;
813     let active_state: String = conn
814         .with_proxy(
815             "org.freedesktop.systemd1",
816             unit_object_path,
817             Duration::from_millis(5000),
818         )
819         .get("org.freedesktop.systemd1.Unit", "ActiveState")
820         .with_context(|| format!("Failed to get ExecMainStatus for {unit}"))?;
822     Ok(matches!(active_state.as_str(), "active" | "activating"))
825 static ACTION: OnceLock<Action> = OnceLock::new();
827 #[derive(Debug)]
828 enum Job {
829     Start,
830     Restart,
831     Reload,
832     Stop,
835 impl std::fmt::Display for Job {
836     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
837         write!(
838             f,
839             "{}",
840             match self {
841                 Job::Start => "start",
842                 Job::Restart => "restart",
843                 Job::Reload => "reload",
844                 Job::Stop => "stop",
845             }
846         )
847     }
850 fn new_dbus_proxies<'a>(
851     conn: &'a LocalConnection,
852 ) -> (
853     Proxy<'a, &'a LocalConnection>,
854     Proxy<'a, &'a LocalConnection>,
855 ) {
856     (
857         conn.with_proxy(
858             "org.freedesktop.systemd1",
859             "/org/freedesktop/systemd1",
860             Duration::from_millis(5000),
861         ),
862         conn.with_proxy(
863             "org.freedesktop.login1",
864             "/org/freedesktop/login1",
865             Duration::from_millis(5000),
866         ),
867     )
870 fn block_on_jobs(
871     conn: &LocalConnection,
872     submitted_jobs: &Rc<RefCell<HashMap<dbus::Path<'static>, Job>>>,
873 ) {
874     while !submitted_jobs.borrow().is_empty() {
875         _ = conn.process(Duration::from_millis(500));
876     }
879 fn remove_file_if_exists(p: impl AsRef<Path>) -> std::io::Result<()> {
880     match std::fs::remove_file(p) {
881         Err(err) if err.kind() != std::io::ErrorKind::NotFound => Err(err),
882         _ => Ok(()),
883     }
886 /// Performs switch-to-configuration functionality for a single non-root user
887 fn do_user_switch(parent_exe: String) -> anyhow::Result<()> {
888     if Path::new(&parent_exe)
889         != Path::new("/proc/self/exe")
890             .canonicalize()
891             .context("Failed to get full path to current executable")?
892             .as_path()
893     {
894         eprintln!(
895             r#"This program is not meant to be called from outside of switch-to-configuration."#
896         );
897         die();
898     }
900     let dbus_conn = LocalConnection::new_session().context("Failed to open dbus connection")?;
901     let (systemd, _) = new_dbus_proxies(&dbus_conn);
903     let nixos_activation_done = Rc::new(RefCell::new(false));
904     let _nixos_activation_done = nixos_activation_done.clone();
905     let jobs_token = systemd
906         .match_signal(
907             move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
908                   _: &LocalConnection,
909                   _: &Message| {
910                 if signal.unit.as_str() == "nixos-activation.service" {
911                     *_nixos_activation_done.borrow_mut() = true;
912                 }
914                 true
915             },
916         )
917         .context("Failed to add signal match for systemd removed jobs")?;
919     // The systemd user session seems to not send a Reloaded signal, so we don't have anything to
920     // wait on here.
921     _ = systemd.reexecute();
923     systemd
924         .restart_unit("nixos-activation.service", "replace")
925         .context("Failed to restart nixos-activation.service")?;
927     while !*nixos_activation_done.borrow() {
928         _ = dbus_conn
929             .process(Duration::from_secs(500))
930             .context("Failed to process dbus messages")?;
931     }
933     dbus_conn
934         .remove_match(jobs_token)
935         .context("Failed to remove jobs token")?;
937     Ok(())
940 /// Performs switch-to-configuration functionality for the entire system
941 fn do_system_switch() -> anyhow::Result<()> {
942     let out = PathBuf::from(required_env("OUT")?);
943     let toplevel = PathBuf::from(required_env("TOPLEVEL")?);
944     let distro_id = required_env("DISTRO_ID")?;
945     let install_bootloader = required_env("INSTALL_BOOTLOADER")?;
946     let locale_archive = required_env("LOCALE_ARCHIVE")?;
947     let new_systemd = PathBuf::from(required_env("SYSTEMD")?);
949     let mut args = std::env::args();
950     let argv0 = args.next().ok_or(anyhow!("no argv[0]"))?;
952     let Some(Ok(action)) = args.next().map(|a| Action::from_str(&a)) else {
953         eprintln!(
954             r#"Usage: {} [switch|boot|test|dry-activate]
955 switch:       make the configuration the boot default and activate now
956 boot:         make the configuration the boot default
957 test:         activate the configuration, but don't make it the boot default
958 dry-activate: show what would be done if this configuration were activated
960             argv0
961                 .split(std::path::MAIN_SEPARATOR_STR)
962                 .last()
963                 .unwrap_or("switch-to-configuration")
964         );
965         std::process::exit(1);
966     };
968     let action = ACTION.get_or_init(|| action);
970     // The action that is to be performed (like switch, boot, test, dry-activate) Also exposed via
971     // environment variable from now on
972     std::env::set_var("NIXOS_ACTION", Into::<&'static str>::into(action));
974     // Expose the locale archive as an environment variable for systemctl and the activation script
975     if !locale_archive.is_empty() {
976         std::env::set_var("LOCALE_ARCHIVE", locale_archive);
977     }
979     let current_system_bin = std::path::PathBuf::from("/run/current-system/sw/bin")
980         .canonicalize()
981         .context("/run/current-system/sw/bin is missing")?;
983     let os_release = parse_os_release().context("Failed to parse os-release")?;
985     let distro_id_re = Regex::new(format!("^\"?{}\"?$", distro_id).as_str())
986         .context("Invalid regex for distro ID")?;
988     // This is a NixOS installation if it has /etc/NIXOS or a proper /etc/os-release.
989     if !Path::new("/etc/NIXOS").is_file()
990         && !os_release
991             .get("ID")
992             .map(|id| distro_id_re.is_match(id))
993             .unwrap_or_default()
994     {
995         eprintln!("This is not a NixOS installation!");
996         die();
997     }
999     std::fs::create_dir_all("/run/nixos").context("Failed to create /run/nixos directory")?;
1000     let perms = std::fs::Permissions::from_mode(0o755);
1001     std::fs::set_permissions("/run/nixos", perms)
1002         .context("Failed to set permissions on /run/nixos directory")?;
1004     let Ok(lock) = std::fs::OpenOptions::new()
1005         .append(true)
1006         .create(true)
1007         .open("/run/nixos/switch-to-configuration.lock")
1008     else {
1009         eprintln!("Could not open lock");
1010         die();
1011     };
1013     let Ok(_lock) = Flock::lock(lock, FlockArg::LockExclusive) else {
1014         eprintln!("Could not acquire lock");
1015         die();
1016     };
1018     if syslog::init(Facility::LOG_USER, LevelFilter::Debug, Some("nixos")).is_err() {
1019         bail!("Failed to initialize logger");
1020     }
1022     // Install or update the bootloader.
1023     if matches!(action, Action::Switch | Action::Boot) {
1024         do_install_bootloader(&install_bootloader, &toplevel)?;
1025     }
1027     // Just in case the new configuration hangs the system, do a sync now.
1028     if std::env::var("NIXOS_NO_SYNC")
1029         .as_deref()
1030         .unwrap_or_default()
1031         != "1"
1032     {
1033         let fd = nix::fcntl::open("/nix/store", OFlag::O_NOCTTY, Mode::S_IROTH)
1034             .context("Failed to open /nix/store")?;
1035         nix::unistd::syncfs(fd).context("Failed to sync /nix/store")?;
1036     }
1038     if *action == Action::Boot {
1039         std::process::exit(0);
1040     }
1042     let current_init_interface_version =
1043         std::fs::read_to_string("/run/current-system/init-interface-version").unwrap_or_default();
1045     let new_init_interface_version =
1046         std::fs::read_to_string(toplevel.join("init-interface-version"))
1047             .context("File init-interface-version should exist")?;
1049     // Check if we can activate the new configuration.
1050     if current_init_interface_version != new_init_interface_version {
1051         eprintln!(
1052             r#"Warning: the new NixOS configuration has an â€˜init’ that is
1053 incompatible with the current configuration.  The new configuration
1054 won't take effect until you reboot the system.
1056         );
1057         std::process::exit(100);
1058     }
1060     // Ignore SIGHUP so that we're not killed if we're running on (say) virtual console 1 and we
1061     // restart the "tty1" unit.
1062     let handler = SigHandler::Handler(handle_sigpipe);
1063     unsafe { signal::signal(Signal::SIGPIPE, handler) }.context("Failed to set SIGPIPE handler")?;
1065     let mut units_to_stop = HashMap::new();
1066     let mut units_to_skip = HashMap::new();
1067     let mut units_to_filter = HashMap::new(); // units not shown
1069     let mut units_to_start = map_from_list_file(START_LIST_FILE);
1070     let mut units_to_restart = map_from_list_file(RESTART_LIST_FILE);
1071     let mut units_to_reload = map_from_list_file(RELOAD_LIST_FILE);
1073     let dbus_conn = LocalConnection::new_system().context("Failed to open dbus connection")?;
1074     let (systemd, logind) = new_dbus_proxies(&dbus_conn);
1076     let submitted_jobs = Rc::new(RefCell::new(HashMap::new()));
1077     let finished_jobs = Rc::new(RefCell::new(HashMap::new()));
1079     let systemd_reload_status = Rc::new(RefCell::new(false));
1081     systemd
1082         .subscribe()
1083         .context("Failed to subscribe to systemd dbus messages")?;
1085     // Wait for the system to have finished booting.
1086     loop {
1087         let system_state: String = systemd
1088             .get("org.freedesktop.systemd1.Manager", "SystemState")
1089             .context("Failed to get system state")?;
1091         match system_state.as_str() {
1092             "running" | "degraded" | "maintenance" => break,
1093             _ => {
1094                 _ = dbus_conn
1095                     .process(Duration::from_millis(500))
1096                     .context("Failed to process dbus messages")?
1097             }
1098         }
1099     }
1101     let _systemd_reload_status = systemd_reload_status.clone();
1102     let reloading_token = systemd
1103         .match_signal(
1104             move |signal: OrgFreedesktopSystemd1ManagerReloading,
1105                   _: &LocalConnection,
1106                   _msg: &Message| {
1107                 *_systemd_reload_status.borrow_mut() = signal.active;
1109                 true
1110             },
1111         )
1112         .context("Failed to add systemd Reloading match")?;
1114     let _submitted_jobs = submitted_jobs.clone();
1115     let _finished_jobs = finished_jobs.clone();
1116     let job_removed_token = systemd
1117         .match_signal(
1118             move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
1119                   _: &LocalConnection,
1120                   _msg: &Message| {
1121                 if let Some(old) = _submitted_jobs.borrow_mut().remove(&signal.job) {
1122                     let mut finished_jobs = _finished_jobs.borrow_mut();
1123                     finished_jobs.insert(signal.job, (signal.unit, old, signal.result));
1124                 }
1126                 true
1127             },
1128         )
1129         .context("Failed to add systemd JobRemoved match")?;
1131     let current_active_units = get_active_units(&systemd)?;
1133     let template_unit_re = Regex::new(r"^(.*)@[^\.]*\.(.*)$")
1134         .context("Invalid regex for matching systemd template units")?;
1135     let unit_name_re = Regex::new(r"^(.*)\.[[:lower:]]*$")
1136         .context("Invalid regex for matching systemd unit names")?;
1138     for (unit, unit_state) in &current_active_units {
1139         let current_unit_file = Path::new("/etc/systemd/system").join(&unit);
1140         let new_unit_file = toplevel.join("etc/systemd/system").join(&unit);
1142         let mut base_unit = unit.clone();
1143         let mut current_base_unit_file = current_unit_file.clone();
1144         let mut new_base_unit_file = new_unit_file.clone();
1146         // Detect template instances
1147         if let Some((Some(template_name), Some(template_instance))) =
1148             template_unit_re.captures(&unit).map(|captures| {
1149                 (
1150                     captures.get(1).map(|c| c.as_str()),
1151                     captures.get(2).map(|c| c.as_str()),
1152                 )
1153             })
1154         {
1155             if !current_unit_file.exists() && !new_unit_file.exists() {
1156                 base_unit = format!("{}@.{}", template_name, template_instance);
1157                 current_base_unit_file = Path::new("/etc/systemd/system").join(&base_unit);
1158                 new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
1159             }
1160         }
1162         let mut base_name = base_unit.as_str();
1163         if let Some(Some(new_base_name)) = unit_name_re
1164             .captures(&base_unit)
1165             .map(|capture| capture.get(1).map(|first| first.as_str()))
1166         {
1167             base_name = new_base_name;
1168         }
1170         if current_base_unit_file.exists()
1171             && (unit_state.state == "active" || unit_state.state == "activating")
1172         {
1173             if new_base_unit_file
1174                 .canonicalize()
1175                 .map(|full_path| full_path == Path::new("/dev/null"))
1176                 .unwrap_or(true)
1177             {
1178                 let current_unit_info = parse_unit(&current_unit_file, &current_base_unit_file)?;
1179                 if parse_systemd_bool(Some(&current_unit_info), "Unit", "X-StopOnRemoval", true) {
1180                     _ = units_to_stop.insert(unit.to_string(), ());
1181                 }
1182             } else if unit.ends_with(".target") {
1183                 let new_unit_info = parse_unit(&new_unit_file, &new_base_unit_file)?;
1185                 // Cause all active target units to be restarted below. This should start most
1186                 // changed units we stop here as well as any new dependencies (including new mounts
1187                 // and swap devices).  FIXME: the suspend target is sometimes active after the
1188                 // system has resumed, which probably should not be the case.  Just ignore it.
1189                 if !matches!(
1190                     unit.as_str(),
1191                     "suspend.target" | "hibernate.target" | "hybrid-sleep.target"
1192                 ) {
1193                     if !(parse_systemd_bool(
1194                         Some(&new_unit_info),
1195                         "Unit",
1196                         "RefuseManualStart",
1197                         false,
1198                     ) || parse_systemd_bool(
1199                         Some(&new_unit_info),
1200                         "Unit",
1201                         "X-OnlyManualStart",
1202                         false,
1203                     )) {
1204                         units_to_start.insert(unit.to_string(), ());
1205                         record_unit(START_LIST_FILE, unit);
1206                         // Don't spam the user with target units that always get started.
1207                         if std::env::var("STC_DISPLAY_ALL_UNITS").as_deref() != Ok("1") {
1208                             units_to_filter.insert(unit.to_string(), ());
1209                         }
1210                     }
1211                 }
1213                 // Stop targets that have X-StopOnReconfiguration set. This is necessary to respect
1214                 // dependency orderings involving targets: if unit X starts after target Y and
1215                 // target Y starts after unit Z, then if X and Z have both changed, then X should
1216                 // be restarted after Z.  However, if target Y is in the "active" state, X and Z
1217                 // will be restarted at the same time because X's dependency on Y is already
1218                 // satisfied.  Thus, we need to stop Y first. Stopping a target generally has no
1219                 // effect on other units (unless there is a PartOf dependency), so this is just a
1220                 // bookkeeping thing to get systemd to do the right thing.
1221                 if parse_systemd_bool(
1222                     Some(&new_unit_info),
1223                     "Unit",
1224                     "X-StopOnReconfiguration",
1225                     false,
1226                 ) {
1227                     units_to_stop.insert(unit.to_string(), ());
1228                 }
1229             } else {
1230                 let current_unit_info = parse_unit(&current_unit_file, &current_base_unit_file)?;
1231                 let new_unit_info = parse_unit(&new_unit_file, &new_base_unit_file)?;
1232                 match compare_units(&current_unit_info, &new_unit_info) {
1233                     UnitComparison::UnequalNeedsRestart => {
1234                         handle_modified_unit(
1235                             &toplevel,
1236                             &unit,
1237                             base_name,
1238                             &new_unit_file,
1239                             &new_base_unit_file,
1240                             Some(&new_unit_info),
1241                             &current_active_units,
1242                             &mut units_to_stop,
1243                             &mut units_to_start,
1244                             &mut units_to_reload,
1245                             &mut units_to_restart,
1246                             &mut units_to_skip,
1247                         )?;
1248                     }
1249                     UnitComparison::UnequalNeedsReload if !units_to_restart.contains_key(unit) => {
1250                         units_to_reload.insert(unit.clone(), ());
1251                         record_unit(RELOAD_LIST_FILE, &unit);
1252                     }
1253                     _ => {}
1254                 }
1255             }
1256         }
1257     }
1259     // Compare the previous and new fstab to figure out which filesystems need a remount or need to
1260     // be unmounted. New filesystems are mounted automatically by starting local-fs.target.
1261     // FIXME: might be nicer if we generated units for all mounts; then we could unify this with
1262     // the unit checking code above.
1263     let (current_filesystems, current_swaps) = std::fs::read_to_string("/etc/fstab")
1264         .map(|fstab| parse_fstab(std::io::Cursor::new(fstab)))
1265         .unwrap_or_default();
1266     let (new_filesystems, new_swaps) = std::fs::read_to_string(toplevel.join("etc/fstab"))
1267         .map(|fstab| parse_fstab(std::io::Cursor::new(fstab)))
1268         .unwrap_or_default();
1270     for (mountpoint, current_filesystem) in current_filesystems {
1271         // Use current version of systemctl binary before daemon is reexeced.
1272         let unit = path_to_unit_name(&current_system_bin, &mountpoint);
1273         if let Some(new_filesystem) = new_filesystems.get(&mountpoint) {
1274             if current_filesystem.fs_type != new_filesystem.fs_type
1275                 || current_filesystem.device != new_filesystem.device
1276             {
1277                 if matches!(mountpoint.as_str(), "/" | "/nix") {
1278                     if current_filesystem.options != new_filesystem.options {
1279                         // Mount options changes, so remount it.
1280                         units_to_reload.insert(unit.to_string(), ());
1281                         record_unit(RELOAD_LIST_FILE, &unit)
1282                     } else {
1283                         // Don't unmount / or /nix if the device changed
1284                         units_to_skip.insert(unit, ());
1285                     }
1286                 } else {
1287                     // Filesystem type or device changed, so unmount and mount it.
1288                     units_to_restart.insert(unit.to_string(), ());
1289                     record_unit(RESTART_LIST_FILE, &unit);
1290                 }
1291             } else if current_filesystem.options != new_filesystem.options {
1292                 // Mount options changes, so remount it.
1293                 units_to_reload.insert(unit.to_string(), ());
1294                 record_unit(RELOAD_LIST_FILE, &unit)
1295             }
1296         } else {
1297             // Filesystem entry disappeared, so unmount it.
1298             units_to_stop.insert(unit, ());
1299         }
1300     }
1302     // Also handles swap devices.
1303     for (device, _) in current_swaps {
1304         if new_swaps.get(&device).is_none() {
1305             // Swap entry disappeared, so turn it off.  Can't use "systemctl stop" here because
1306             // systemd has lots of alias units that prevent a stop from actually calling "swapoff".
1307             if *action == Action::DryActivate {
1308                 eprintln!("would stop swap device: {}", &device);
1309             } else {
1310                 eprintln!("stopping swap device: {}", &device);
1311                 let c_device = std::ffi::CString::new(device.clone())
1312                     .context("failed to convert device to cstring")?;
1313                 if unsafe { nix::libc::swapoff(c_device.as_ptr()) } != 0 {
1314                     let err = std::io::Error::last_os_error();
1315                     eprintln!("Failed to stop swapping to {device}: {err}");
1316                 }
1317             }
1318         }
1319         // FIXME: update swap options (i.e. its priority).
1320     }
1322     // Should we have systemd re-exec itself?
1323     let current_pid1_path = Path::new("/proc/1/exe")
1324         .canonicalize()
1325         .unwrap_or_else(|_| PathBuf::from("/unknown"));
1326     let current_systemd_system_config = Path::new("/etc/systemd/system.conf")
1327         .canonicalize()
1328         .unwrap_or_else(|_| PathBuf::from("/unknown"));
1329     let Ok(new_pid1_path) = new_systemd.join("lib/systemd/systemd").canonicalize() else {
1330         die();
1331     };
1332     let new_systemd_system_config = toplevel
1333         .join("etc/systemd/system.conf")
1334         .canonicalize()
1335         .unwrap_or_else(|_| PathBuf::from("/unknown"));
1337     let restart_systemd = current_pid1_path != new_pid1_path
1338         || current_systemd_system_config != new_systemd_system_config;
1340     let units_to_stop_filtered = filter_units(&units_to_filter, &units_to_stop);
1342     // Show dry-run actions.
1343     if *action == Action::DryActivate {
1344         if !units_to_stop_filtered.is_empty() {
1345             let mut units = units_to_stop_filtered
1346                 .keys()
1347                 .into_iter()
1348                 .map(String::as_str)
1349                 .collect::<Vec<&str>>();
1350             units.sort_by_key(|name| name.to_lowercase());
1351             eprintln!("would stop the following units: {}", units.join(", "));
1352         }
1354         if !units_to_skip.is_empty() {
1355             let mut units = units_to_skip
1356                 .keys()
1357                 .into_iter()
1358                 .map(String::as_str)
1359                 .collect::<Vec<&str>>();
1360             units.sort_by_key(|name| name.to_lowercase());
1361             eprintln!(
1362                 "would NOT stop the following changed units: {}",
1363                 units.join(", ")
1364             );
1365         }
1367         eprintln!("would activate the configuration...");
1368         _ = std::process::Command::new(out.join("dry-activate"))
1369             .arg(&out)
1370             .spawn()
1371             .map(|mut child| child.wait());
1373         // Handle the activation script requesting the restart or reload of a unit.
1374         for unit in std::fs::read_to_string(DRY_RESTART_BY_ACTIVATION_LIST_FILE)
1375             .unwrap_or_default()
1376             .lines()
1377         {
1378             let current_unit_file = Path::new("/etc/systemd/system").join(unit);
1379             let new_unit_file = toplevel.join("etc/systemd/system").join(unit);
1380             let mut base_unit = unit.to_string();
1381             let mut new_base_unit_file = new_unit_file.clone();
1383             // Detect template instances.
1384             if let Some((Some(template_name), Some(template_instance))) =
1385                 template_unit_re.captures(&unit).map(|captures| {
1386                     (
1387                         captures.get(1).map(|c| c.as_str()),
1388                         captures.get(2).map(|c| c.as_str()),
1389                     )
1390                 })
1391             {
1392                 if !current_unit_file.exists() && !new_unit_file.exists() {
1393                     base_unit = format!("{}@.{}", template_name, template_instance);
1394                     new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
1395                 }
1396             }
1398             let mut base_name = base_unit.as_str();
1399             if let Some(Some(new_base_name)) = unit_name_re
1400                 .captures(&base_unit)
1401                 .map(|capture| capture.get(1).map(|first| first.as_str()))
1402             {
1403                 base_name = new_base_name;
1404             }
1406             // Start units if they were not active previously
1407             if !current_active_units.contains_key(unit) {
1408                 units_to_start.insert(unit.to_string(), ());
1409                 continue;
1410             }
1412             handle_modified_unit(
1413                 &toplevel,
1414                 unit,
1415                 base_name,
1416                 &new_unit_file,
1417                 &new_base_unit_file,
1418                 None,
1419                 &current_active_units,
1420                 &mut units_to_stop,
1421                 &mut units_to_start,
1422                 &mut units_to_reload,
1423                 &mut units_to_restart,
1424                 &mut units_to_skip,
1425             )?;
1426         }
1428         remove_file_if_exists(DRY_RESTART_BY_ACTIVATION_LIST_FILE)
1429             .with_context(|| format!("Failed to remove {}", DRY_RESTART_BY_ACTIVATION_LIST_FILE))?;
1431         for unit in std::fs::read_to_string(DRY_RELOAD_BY_ACTIVATION_LIST_FILE)
1432             .unwrap_or_default()
1433             .lines()
1434         {
1435             if current_active_units.contains_key(unit)
1436                 && !units_to_restart.contains_key(unit)
1437                 && !units_to_stop.contains_key(unit)
1438             {
1439                 units_to_reload.insert(unit.to_string(), ());
1440                 record_unit(RELOAD_LIST_FILE, unit);
1441             }
1442         }
1444         remove_file_if_exists(DRY_RELOAD_BY_ACTIVATION_LIST_FILE)
1445             .with_context(|| format!("Failed to remove {}", DRY_RELOAD_BY_ACTIVATION_LIST_FILE))?;
1447         if restart_systemd {
1448             eprintln!("would restart systemd");
1449         }
1451         if !units_to_reload.is_empty() {
1452             let mut units = units_to_reload
1453                 .keys()
1454                 .into_iter()
1455                 .map(String::as_str)
1456                 .collect::<Vec<&str>>();
1457             units.sort_by_key(|name| name.to_lowercase());
1458             eprintln!("would reload the following units: {}", units.join(", "));
1459         }
1461         if !units_to_restart.is_empty() {
1462             let mut units = units_to_restart
1463                 .keys()
1464                 .into_iter()
1465                 .map(String::as_str)
1466                 .collect::<Vec<&str>>();
1467             units.sort_by_key(|name| name.to_lowercase());
1468             eprintln!("would restart the following units: {}", units.join(", "));
1469         }
1471         let units_to_start_filtered = filter_units(&units_to_filter, &units_to_start);
1472         if !units_to_start_filtered.is_empty() {
1473             let mut units = units_to_start_filtered
1474                 .keys()
1475                 .into_iter()
1476                 .map(String::as_str)
1477                 .collect::<Vec<&str>>();
1478             units.sort_by_key(|name| name.to_lowercase());
1479             eprintln!("would start the following units: {}", units.join(", "));
1480         }
1482         std::process::exit(0);
1483     }
1485     log::info!("switching to system configuration {}", toplevel.display());
1487     if !units_to_stop.is_empty() {
1488         if !units_to_stop_filtered.is_empty() {
1489             let mut units = units_to_stop_filtered
1490                 .keys()
1491                 .into_iter()
1492                 .map(String::as_str)
1493                 .collect::<Vec<&str>>();
1494             units.sort_by_key(|name| name.to_lowercase());
1495             eprintln!("stopping the following units: {}", units.join(", "));
1496         }
1498         for unit in units_to_stop.keys() {
1499             match systemd.stop_unit(unit, "replace") {
1500                 Ok(job_path) => {
1501                     let mut j = submitted_jobs.borrow_mut();
1502                     j.insert(job_path.to_owned(), Job::Stop);
1503                 }
1504                 Err(_) => {}
1505             };
1506         }
1508         block_on_jobs(&dbus_conn, &submitted_jobs);
1509     }
1511     if !units_to_skip.is_empty() {
1512         let mut units = units_to_skip
1513             .keys()
1514             .into_iter()
1515             .map(String::as_str)
1516             .collect::<Vec<&str>>();
1517         units.sort_by_key(|name| name.to_lowercase());
1518         eprintln!(
1519             "NOT restarting the following changed units: {}",
1520             units.join(", "),
1521         );
1522     }
1524     // Wait for all stop jobs to finish
1525     block_on_jobs(&dbus_conn, &submitted_jobs);
1527     let mut exit_code = 0;
1529     // Activate the new configuration (i.e., update /etc, make accounts, and so on).
1530     eprintln!("activating the configuration...");
1531     match std::process::Command::new(out.join("activate"))
1532         .arg(&out)
1533         .spawn()
1534         .map(|mut child| child.wait())
1535     {
1536         Ok(Ok(status)) if status.success() => {}
1537         Err(_) => {
1538             // allow toplevel to not have an activation script
1539         }
1540         _ => {
1541             eprintln!("Failed to run activate script");
1542             exit_code = 2;
1543         }
1544     }
1546     // Handle the activation script requesting the restart or reload of a unit.
1547     for unit in std::fs::read_to_string(RESTART_BY_ACTIVATION_LIST_FILE)
1548         .unwrap_or_default()
1549         .lines()
1550     {
1551         let new_unit_file = toplevel.join("etc/systemd/system").join(unit);
1552         let mut base_unit = unit.to_string();
1553         let mut new_base_unit_file = new_unit_file.clone();
1555         // Detect template instances.
1556         if let Some((Some(template_name), Some(template_instance))) =
1557             template_unit_re.captures(&unit).map(|captures| {
1558                 (
1559                     captures.get(1).map(|c| c.as_str()),
1560                     captures.get(2).map(|c| c.as_str()),
1561                 )
1562             })
1563         {
1564             if !new_unit_file.exists() {
1565                 base_unit = format!("{}@.{}", template_name, template_instance);
1566                 new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
1567             }
1568         }
1570         let mut base_name = base_unit.as_str();
1571         if let Some(Some(new_base_name)) = unit_name_re
1572             .captures(&base_unit)
1573             .map(|capture| capture.get(1).map(|first| first.as_str()))
1574         {
1575             base_name = new_base_name;
1576         }
1578         // Start units if they were not active previously
1579         if !current_active_units.contains_key(unit) {
1580             units_to_start.insert(unit.to_string(), ());
1581             record_unit(START_LIST_FILE, unit);
1582             continue;
1583         }
1585         handle_modified_unit(
1586             &toplevel,
1587             unit,
1588             base_name,
1589             &new_unit_file,
1590             &new_base_unit_file,
1591             None,
1592             &current_active_units,
1593             &mut units_to_stop,
1594             &mut units_to_start,
1595             &mut units_to_reload,
1596             &mut units_to_restart,
1597             &mut units_to_skip,
1598         )?;
1599     }
1601     // We can remove the file now because it has been propagated to the other restart/reload files
1602     remove_file_if_exists(RESTART_BY_ACTIVATION_LIST_FILE)
1603         .with_context(|| format!("Failed to remove {}", RESTART_BY_ACTIVATION_LIST_FILE))?;
1605     for unit in std::fs::read_to_string(RELOAD_BY_ACTIVATION_LIST_FILE)
1606         .unwrap_or_default()
1607         .lines()
1608     {
1609         if current_active_units.contains_key(unit)
1610             && !units_to_restart.contains_key(unit)
1611             && !units_to_stop.contains_key(unit)
1612         {
1613             units_to_reload.insert(unit.to_string(), ());
1614             record_unit(RELOAD_LIST_FILE, unit);
1615         }
1616     }
1618     // We can remove the file now because it has been propagated to the other reload file
1619     remove_file_if_exists(RELOAD_BY_ACTIVATION_LIST_FILE)
1620         .with_context(|| format!("Failed to remove {}", RELOAD_BY_ACTIVATION_LIST_FILE))?;
1622     // Restart systemd if necessary. Note that this is done using the current version of systemd,
1623     // just in case the new one has trouble communicating with the running pid 1.
1624     if restart_systemd {
1625         eprintln!("restarting systemd...");
1626         _ = systemd.reexecute(); // we don't get a dbus reply here
1628         while !*systemd_reload_status.borrow() {
1629             _ = dbus_conn
1630                 .process(Duration::from_millis(500))
1631                 .context("Failed to process dbus messages")?;
1632         }
1633     }
1635     // Forget about previously failed services.
1636     systemd
1637         .reset_failed()
1638         .context("Failed to reset failed units")?;
1640     // Make systemd reload its units.
1641     _ = systemd.reload(); // we don't get a dbus reply here
1642     while !*systemd_reload_status.borrow() {
1643         _ = dbus_conn
1644             .process(Duration::from_millis(500))
1645             .context("Failed to process dbus messages")?;
1646     }
1648     dbus_conn
1649         .remove_match(reloading_token)
1650         .context("Failed to cleanup systemd Reloading match")?;
1652     // Reload user units
1653     match logind.list_users() {
1654         Err(err) => {
1655             eprintln!("Unable to list users with logind: {err}");
1656             die();
1657         }
1658         Ok(users) => {
1659             for (uid, name, user_dbus_path) in users {
1660                 let gid: u32 = dbus_conn
1661                     .with_proxy(
1662                         "org.freedesktop.login1",
1663                         &user_dbus_path,
1664                         Duration::from_millis(5000),
1665                     )
1666                     .get("org.freedesktop.login1.User", "GID")
1667                     .with_context(|| format!("Failed to get GID for {name}"))?;
1669                 eprintln!("reloading user units for {}...", name);
1670                 let myself = Path::new("/proc/self/exe")
1671                     .canonicalize()
1672                     .context("Failed to get full path to /proc/self/exe")?;
1674                 std::process::Command::new(&myself)
1675                     .uid(uid)
1676                     .gid(gid)
1677                     .env("XDG_RUNTIME_DIR", format!("/run/user/{}", uid))
1678                     .env("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE", &myself)
1679                     .spawn()
1680                     .with_context(|| format!("Failed to spawn user activation for {name}"))?
1681                     .wait()
1682                     .with_context(|| format!("Failed to run user activation for {name}"))?;
1683             }
1684         }
1685     }
1687     // Restart sysinit-reactivation.target. This target only exists to restart services ordered
1688     // before sysinit.target. We cannot use X-StopOnReconfiguration to restart sysinit.target
1689     // because then ALL services of the system would be restarted since all normal services have a
1690     // default dependency on sysinit.target. sysinit-reactivation.target ensures that services
1691     // ordered BEFORE sysinit.target get re-started in the correct order. Ordering between these
1692     // services is respected.
1693     eprintln!("restarting {SYSINIT_REACTIVATION_TARGET}");
1694     match systemd.restart_unit(SYSINIT_REACTIVATION_TARGET, "replace") {
1695         Ok(job_path) => {
1696             let mut jobs = submitted_jobs.borrow_mut();
1697             jobs.insert(job_path, Job::Restart);
1698         }
1699         Err(err) => {
1700             eprintln!("Failed to restart {SYSINIT_REACTIVATION_TARGET}: {err}");
1701             exit_code = 4;
1702         }
1703     }
1705     // Wait for the restart job of sysinit-reactivation.service to finish
1706     block_on_jobs(&dbus_conn, &submitted_jobs);
1708     // Before reloading we need to ensure that the units are still active. They may have been
1709     // deactivated because one of their requirements got stopped. If they are inactive but should
1710     // have been reloaded, the user probably expects them to be started.
1711     if !units_to_reload.is_empty() {
1712         for (unit, _) in units_to_reload.clone() {
1713             if !unit_is_active(&dbus_conn, &unit)? {
1714                 // Figure out if we need to start the unit
1715                 let unit_info = parse_unit(
1716                     toplevel.join("etc/systemd/system").join(&unit).as_path(),
1717                     toplevel.join("etc/systemd/system").join(&unit).as_path(),
1718                 )?;
1719                 if !parse_systemd_bool(Some(&unit_info), "Unit", "RefuseManualStart", false)
1720                     || parse_systemd_bool(Some(&unit_info), "Unit", "X-OnlyManualStart", false)
1721                 {
1722                     units_to_start.insert(unit.clone(), ());
1723                     record_unit(START_LIST_FILE, &unit);
1724                 }
1725                 // Don't reload the unit, reloading would fail
1726                 units_to_reload.remove(&unit);
1727                 unrecord_unit(RELOAD_LIST_FILE, &unit);
1728             }
1729         }
1730     }
1732     // Reload units that need it. This includes remounting changed mount units.
1733     if !units_to_reload.is_empty() {
1734         let mut units = units_to_reload
1735             .keys()
1736             .into_iter()
1737             .map(String::as_str)
1738             .collect::<Vec<&str>>();
1739         units.sort_by_key(|name| name.to_lowercase());
1740         eprintln!("reloading the following units: {}", units.join(", "));
1742         for unit in units {
1743             match systemd.reload_unit(unit, "replace") {
1744                 Ok(job_path) => {
1745                     submitted_jobs
1746                         .borrow_mut()
1747                         .insert(job_path.clone(), Job::Reload);
1748                 }
1749                 Err(err) => {
1750                     eprintln!("Failed to reload {unit}: {err}");
1751                     exit_code = 4;
1752                 }
1753             }
1754         }
1756         block_on_jobs(&dbus_conn, &submitted_jobs);
1758         remove_file_if_exists(RELOAD_LIST_FILE)
1759             .with_context(|| format!("Failed to remove {}", RELOAD_LIST_FILE))?;
1760     }
1762     // Restart changed services (those that have to be restarted rather than stopped and started).
1763     if !units_to_restart.is_empty() {
1764         let mut units = units_to_restart
1765             .keys()
1766             .into_iter()
1767             .map(String::as_str)
1768             .collect::<Vec<&str>>();
1769         units.sort_by_key(|name| name.to_lowercase());
1770         eprintln!("restarting the following units: {}", units.join(", "));
1772         for unit in units {
1773             match systemd.restart_unit(unit, "replace") {
1774                 Ok(job_path) => {
1775                     let mut jobs = submitted_jobs.borrow_mut();
1776                     jobs.insert(job_path, Job::Restart);
1777                 }
1778                 Err(err) => {
1779                     eprintln!("Failed to restart {unit}: {err}");
1780                     exit_code = 4;
1781                 }
1782             }
1783         }
1785         block_on_jobs(&dbus_conn, &submitted_jobs);
1787         remove_file_if_exists(RESTART_LIST_FILE)
1788             .with_context(|| format!("Failed to remove {}", RESTART_LIST_FILE))?;
1789     }
1791     // Start all active targets, as well as changed units we stopped above. The latter is necessary
1792     // because some may not be dependencies of the targets (i.e., they were manually started).
1793     // FIXME: detect units that are symlinks to other units.  We shouldn't start both at the same
1794     // time because we'll get a "Failed to add path to set" error from systemd.
1795     let units_to_start_filtered = filter_units(&units_to_filter, &units_to_start);
1796     if !units_to_start_filtered.is_empty() {
1797         let mut units = units_to_start_filtered
1798             .keys()
1799             .into_iter()
1800             .map(String::as_str)
1801             .collect::<Vec<&str>>();
1802         units.sort_by_key(|name| name.to_lowercase());
1803         eprintln!("starting the following units: {}", units.join(", "));
1804     }
1806     for unit in units_to_start.keys() {
1807         match systemd.start_unit(unit, "replace") {
1808             Ok(job_path) => {
1809                 let mut jobs = submitted_jobs.borrow_mut();
1810                 jobs.insert(job_path, Job::Start);
1811             }
1812             Err(err) => {
1813                 eprintln!("Failed to start {unit}: {err}");
1814                 exit_code = 4;
1815             }
1816         }
1817     }
1819     block_on_jobs(&dbus_conn, &submitted_jobs);
1821     remove_file_if_exists(START_LIST_FILE)
1822         .with_context(|| format!("Failed to remove {}", START_LIST_FILE))?;
1824     for (unit, job, result) in finished_jobs.borrow().values() {
1825         match result.as_str() {
1826             "timeout" | "failed" | "dependency" => {
1827                 eprintln!("Failed to {} {}", job, unit);
1828                 exit_code = 4;
1829             }
1830             _ => {}
1831         }
1832     }
1834     dbus_conn
1835         .remove_match(job_removed_token)
1836         .context("Failed to cleanup systemd job match")?;
1838     // Print failed and new units.
1839     let mut failed_units = Vec::new();
1840     let mut new_units = Vec::new();
1842     // NOTE: We want switch-to-configuration to be able to report to the user any units that failed
1843     // to start or units that systemd had to restart due to having previously failed. This is
1844     // inherently a race condition between how long our program takes to run and how long the unit
1845     // in question takes to potentially fail. The amount of time we wait for new messages on the
1846     // bus to settle is purely tuned so that this program is compatible with the Perl
1847     // implementation.
1848     //
1849     // Wait for events from systemd to settle. process() will return true if we have received any
1850     // messages on the bus.
1851     while dbus_conn
1852         .process(Duration::from_millis(250))
1853         .unwrap_or_default()
1854     {}
1856     let new_active_units = get_active_units(&systemd)?;
1858     for (unit, unit_state) in new_active_units {
1859         if &unit_state.state == "failed" {
1860             failed_units.push(unit);
1861             continue;
1862         }
1864         if unit_state.substate == "auto-restart" && unit.ends_with(".service") {
1865             // A unit in auto-restart substate is a failure *if* it previously failed to start
1866             let unit_object_path = systemd
1867                 .get_unit(&unit)
1868                 .with_context(|| format!("Failed to get unit info for {unit}"))?;
1869             let exec_main_status: i32 = dbus_conn
1870                 .with_proxy(
1871                     "org.freedesktop.systemd1",
1872                     unit_object_path,
1873                     Duration::from_millis(5000),
1874                 )
1875                 .get("org.freedesktop.systemd1.Service", "ExecMainStatus")
1876                 .with_context(|| format!("Failed to get ExecMainStatus for {unit}"))?;
1878             if exec_main_status != 0 {
1879                 failed_units.push(unit);
1880                 continue;
1881             }
1882         }
1884         // Ignore scopes since they are not managed by this script but rather created and managed
1885         // by third-party services via the systemd dbus API. This only lists units that are not
1886         // failed (including ones that are in auto-restart but have not failed previously)
1887         if unit_state.state != "failed"
1888             && !current_active_units.contains_key(&unit)
1889             && !unit.ends_with(".scope")
1890         {
1891             new_units.push(unit);
1892         }
1893     }
1895     if !new_units.is_empty() {
1896         new_units.sort_by_key(|name| name.to_lowercase());
1897         eprintln!(
1898             "the following new units were started: {}",
1899             new_units.join(", ")
1900         );
1901     }
1903     if !failed_units.is_empty() {
1904         failed_units.sort_by_key(|name| name.to_lowercase());
1905         eprintln!(
1906             "warning: the following units failed: {}",
1907             failed_units.join(", ")
1908         );
1909         _ = std::process::Command::new(new_systemd.join("bin/systemctl"))
1910             .arg("status")
1911             .arg("--no-pager")
1912             .arg("--full")
1913             .args(failed_units)
1914             .spawn()
1915             .map(|mut child| child.wait());
1917         exit_code = 4;
1918     }
1920     if exit_code == 0 {
1921         log::info!(
1922             "finished switching to system configuration {}",
1923             toplevel.display()
1924         );
1925     } else {
1926         log::error!(
1927             "switching to system configuration {} failed (status {})",
1928             toplevel.display(),
1929             exit_code
1930         );
1931     }
1933     std::process::exit(exit_code);
1936 fn main() -> anyhow::Result<()> {
1937     match (
1938         unsafe { nix::libc::geteuid() },
1939         std::env::var("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE").ok(),
1940     ) {
1941         (0, None) => do_system_switch(),
1942         (1..=u32::MAX, None) => bail!("This program does not support being ran outside of the switch-to-configuration environment"),
1943         (_, Some(parent_exe)) => do_user_switch(parent_exe),
1944     }
1947 #[cfg(test)]
1948 mod tests {
1949     use std::collections::HashMap;
1951     #[test]
1952     fn parse_fstab() {
1953         {
1954             let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(""));
1955             assert!(filesystems.is_empty());
1956             assert!(swaps.is_empty());
1957         }
1959         {
1960             let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
1961                 r#"\
1962 invalid
1963                     "#,
1964             ));
1965             assert!(filesystems.is_empty());
1966             assert!(swaps.is_empty());
1967         }
1969         {
1970             let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
1971                 r#"\
1972 # This is a generated file.  Do not edit!
1974 # To make changes, edit the fileSystems and swapDevices NixOS options
1975 # in your /etc/nixos/configuration.nix file.
1977 # <file system> <mount point>   <type>  <options>       <dump>  <pass>
1979 # Filesystems.
1980 /dev/mapper/root / btrfs x-initrd.mount,compress=zstd,noatime,defaults 0 0
1981 /dev/disk/by-partlabel/BOOT /boot vfat x-systemd.automount 0 2
1982 /dev/disk/by-partlabel/home /home ext4 defaults 0 2
1983 /dev/mapper/usr /nix/.ro-store erofs x-initrd.mount,ro 0 2
1986 # Swap devices.
1987                     "#,
1988             ));
1989             assert_eq!(filesystems.len(), 4);
1990             assert_eq!(swaps.len(), 0);
1991             let home_fs = filesystems.get("/home").unwrap();
1992             assert_eq!(home_fs.fs_type, "ext4");
1993             assert_eq!(home_fs.device, "/dev/disk/by-partlabel/home");
1994             assert_eq!(home_fs.options, "defaults");
1995         }
1996     }
1998     #[test]
1999     fn filter_units() {
2000         assert_eq!(
2001             super::filter_units(&HashMap::from([]), &HashMap::from([])),
2002             HashMap::from([])
2003         );
2005         assert_eq!(
2006             super::filter_units(
2007                 &HashMap::from([("foo".to_string(), ())]),
2008                 &HashMap::from([("foo".to_string(), ()), ("bar".to_string(), ())])
2009             ),
2010             HashMap::from([("bar".to_string(), ())])
2011         );
2012     }
2014     #[test]
2015     fn compare_units() {
2016         {
2017             assert!(
2018                 super::compare_units(&HashMap::from([]), &HashMap::from([]))
2019                     == super::UnitComparison::Equal
2020             );
2022             assert!(
2023                 super::compare_units(
2024                     &HashMap::from([("Unit".to_string(), HashMap::from([]))]),
2025                     &HashMap::from([])
2026                 ) == super::UnitComparison::Equal
2027             );
2029             assert!(
2030                 super::compare_units(
2031                     &HashMap::from([(
2032                         "Unit".to_string(),
2033                         HashMap::from([(
2034                             "X-Reload-Triggers".to_string(),
2035                             vec!["foobar".to_string()]
2036                         )])
2037                     )]),
2038                     &HashMap::from([])
2039                 ) == super::UnitComparison::Equal
2040             );
2041         }
2043         {
2044             assert!(
2045                 super::compare_units(
2046                     &HashMap::from([("foobar".to_string(), HashMap::from([]))]),
2047                     &HashMap::from([])
2048                 ) == super::UnitComparison::UnequalNeedsRestart
2049             );
2051             assert!(
2052                 super::compare_units(
2053                     &HashMap::from([(
2054                         "Mount".to_string(),
2055                         HashMap::from([("Options".to_string(), vec![])])
2056                     )]),
2057                     &HashMap::from([(
2058                         "Mount".to_string(),
2059                         HashMap::from([("Options".to_string(), vec!["ro".to_string()])])
2060                     )])
2061                 ) == super::UnitComparison::UnequalNeedsReload
2062             );
2063         }
2065         {
2066             assert!(
2067                 super::compare_units(
2068                     &HashMap::from([]),
2069                     &HashMap::from([(
2070                         "Unit".to_string(),
2071                         HashMap::from([(
2072                             "X-Reload-Triggers".to_string(),
2073                             vec!["foobar".to_string()]
2074                         )])
2075                     )])
2076                 ) == super::UnitComparison::UnequalNeedsReload
2077             );
2079             assert!(
2080                 super::compare_units(
2081                     &HashMap::from([(
2082                         "Unit".to_string(),
2083                         HashMap::from([(
2084                             "X-Reload-Triggers".to_string(),
2085                             vec!["foobar".to_string()]
2086                         )])
2087                     )]),
2088                     &HashMap::from([(
2089                         "Unit".to_string(),
2090                         HashMap::from([(
2091                             "X-Reload-Triggers".to_string(),
2092                             vec!["barfoo".to_string()]
2093                         )])
2094                     )])
2095                 ) == super::UnitComparison::UnequalNeedsReload
2096             );
2098             assert!(
2099                 super::compare_units(
2100                     &HashMap::from([(
2101                         "Mount".to_string(),
2102                         HashMap::from([("Type".to_string(), vec!["ext4".to_string()])])
2103                     )]),
2104                     &HashMap::from([(
2105                         "Mount".to_string(),
2106                         HashMap::from([("Type".to_string(), vec!["btrfs".to_string()])])
2107                     )])
2108                 ) == super::UnitComparison::UnequalNeedsRestart
2109             );
2110         }
2111     }
2113     #[test]
2114     fn parse_systemd_ini() {
2115         // Ensure we don't attempt to unescape content in unit files.
2116         // https://github.com/NixOS/nixpkgs/issues/315602
2117         {
2118             let mut unit_info = HashMap::new();
2120             let test_unit = std::io::Cursor::new(
2121                 r#"[Unit]
2122 After=dev-disk-by\x2dlabel-root.device
2124             );
2125             super::parse_systemd_ini(&mut unit_info, test_unit).unwrap();
2127             assert_eq!(
2128                 unit_info
2129                     .get("Unit")
2130                     .unwrap()
2131                     .get("After")
2132                     .unwrap()
2133                     .first()
2134                     .unwrap(),
2135                 "dev-disk-by\\x2dlabel-root.device"
2136             );
2137         }
2138     }