4 io::{BufRead, Read, Write},
5 os::unix::{fs::PermissionsExt, process::CommandExt},
13 use anyhow::{anyhow, bail, Context, Result};
15 blocking::{stdintf::org_freedesktop_dbus::Properties, LocalConnection, Proxy},
19 use ini::{Ini, ParseOption};
22 fcntl::{Flock, FlockArg, OFlag},
24 signal::{self, SigHandler, Signal},
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"));
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,
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)| {
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, mut unit_file: impl Read) -> Result<()> {
220 let mut unit_file_content = String::new();
222 .read_to_string(&mut unit_file_content)
223 .context("Failed to read unit file")?;
225 let ini = Ini::load_from_str_opt(
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,
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 {
242 if section == "Install" {
243 // Skip the [Install] section because it has no relevant keys for us
247 let section_map = if let Some(section_map) = data.get_mut(section) {
250 data.insert(section.to_string(), HashMap::new());
251 data.get_mut(section)
252 .ok_or(anyhow!("section name should exist in hashmap"))?
255 for (ini_key, _) in properties {
256 let values = properties.get_all(ini_key);
260 .collect::<Vec<String>>();
262 // Go over all values
263 let mut new_vals = Vec::new();
264 let mut clear_existing = false;
267 // If a value is empty, it's an override that tells us to clean the value
270 clear_existing = true;
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),
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(|| {
299 "Failed to parse systemd unit file {}",
300 base_unit_path.display()
305 glob(&format!("{}.d/*.conf", base_unit_path.display())).context("Invalid glob pattern")?
307 let Ok(entry) = entry else {
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)?;
316 // Handle drop-in template-unit instance overrides
317 if unit_file != base_unit_path {
319 glob(&format!("{}.d/*.conf", unit_file.display())).context("Invalid glob pattern")?
321 let Ok(entry) = entry else {
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)?;
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>,
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| {
346 .map(|last| matches!(last.as_str(), "1" | "yes" | "true" | "on"))
356 #[derive(Debug, PartialEq)]
357 enum UnitComparison {
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(
387 .map(|name| (name, ())),
390 let mut section_cmp = new_unit.keys().fold(HashMap::new(), |mut acc, key| {
391 acc.insert(key.as_str(), ());
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
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;
407 continue; // check the next section
409 return UnitComparison::UnequalNeedsRestart;
413 section_cmp.remove(section_name.as_str());
415 // Comparison hash for the section contents
416 let mut ini_cmp = new_unit
419 section_val.keys().fold(HashMap::new(), |mut acc, ini_key| {
420 acc.insert(ini_key.as_str(), ());
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
431 .map(|section| section.get(ini_key))
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()) {
438 return UnitComparison::UnequalNeedsRestart;
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;
447 } else if unit_section_ignores.contains_key(ini_key.as_str()) {
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;
458 return UnitComparison::UnequalNeedsRestart;
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) {
471 return UnitComparison::UnequalNeedsRestart;
475 return UnitComparison::UnequalNeedsRestart;
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;
493 return UnitComparison::UnequalNeedsRestart;
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(
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, ()>,
516 let use_restart_as_stop_and_start = new_unit_info.is_none();
520 "sysinit.target" | "basic.target" | "multi-user.target" | "graphical.target"
521 ) || unit.ends_with(".unit")
522 || unit.ends_with(".slice")
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
534 if unit == "-.mount" || unit == "nix.mount" {
535 units_to_reload.insert(unit.to_string(), ());
536 record_unit(RELOAD_LIST_FILE, unit);
538 units_to_restart.insert(unit.to_string(), ());
539 record_unit(RESTART_LIST_FILE, unit);
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
547 let fallback = parse_unit(new_unit_file, new_base_unit_file)?;
548 let new_unit_info = if new_unit_info.is_some() {
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)
559 units_to_stop.contains_key(unit)
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)
568 units_to_skip.insert(unit.to_string(), ());
570 // It doesn't make sense to stop and start non-services because they can't have
572 if !parse_systemd_bool(new_unit_info, "Service", "X-StopIfChanged", true)
573 || !unit.ends_with(".service")
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);
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| {
590 .map(|service_section| service_section.get("Sockets"))
602 if sockets.is_empty() {
603 sockets.push(format!("{}.socket", base_name));
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(), ());
613 units_to_stop.insert(socket.to_string(), ());
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);
622 units_to_start.insert(socket.to_string(), ());
623 record_unit(START_LIST_FILE, socket);
626 socket_activated = true;
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);
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);
646 units_to_start.insert(unit.to_string(), ());
647 record_unit(START_LIST_FILE, unit);
651 if use_restart_as_stop_and_start {
652 units_to_restart.insert(unit.to_string(), ());
654 units_to_stop.insert(unit.to_string(), ());
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);
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}");
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()
691 .filter(|line| line != &unit)
692 .for_each(|line| _ = writeln!(&mut f, "{line}"))
698 fn map_from_list_file(p: impl AsRef<Path>) -> HashMap<String, ()> {
699 std::fs::read_to_string(p)
702 .filter(|line| !line.is_empty())
704 .fold(HashMap::new(), |mut acc, line| {
705 acc.insert(line.to_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
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 {
734 if line.contains('#') {
738 let mut split = line.split_whitespace();
739 let (Some(device), Some(mountpoint), Some(fs_type), options) = (
743 split.next().unwrap_or_default(),
748 if fs_type == "swap" {
749 swaps.insert(device.to_string(), Swap(options.to_string()));
752 mountpoint.to_string(),
754 device: device.to_string(),
755 fs_type: fs_type.to_string(),
756 options: options.to_string(),
765 // Converts a path to the name of a systemd mount unit that would be responsible for mounting this
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")
774 eprintln!("Unable to escape {}!", path);
778 let Ok(unit) = String::from_utf8(output.stdout) else {
779 eprintln!("Unable to convert systemd-espape output to valid UTF-8");
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`.
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(), ());
803 fn unit_is_active<'a>(conn: &LocalConnection, unit: &str) -> Result<bool> {
804 let unit_object_path = conn
806 "org.freedesktop.systemd1",
807 "/org/freedesktop/systemd1",
808 Duration::from_millis(5000),
811 .with_context(|| format!("Failed to get unit {unit}"))?;
813 let active_state: String = conn
815 "org.freedesktop.systemd1",
817 Duration::from_millis(5000),
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();
835 impl std::fmt::Display for Job {
836 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
841 Job::Start => "start",
842 Job::Restart => "restart",
843 Job::Reload => "reload",
850 fn new_dbus_proxies<'a>(
851 conn: &'a LocalConnection,
853 Proxy<'a, &'a LocalConnection>,
854 Proxy<'a, &'a LocalConnection>,
858 "org.freedesktop.systemd1",
859 "/org/freedesktop/systemd1",
860 Duration::from_millis(5000),
863 "org.freedesktop.login1",
864 "/org/freedesktop/login1",
865 Duration::from_millis(5000),
871 conn: &LocalConnection,
872 submitted_jobs: &Rc<RefCell<HashMap<dbus::Path<'static>, Job>>>,
874 while !submitted_jobs.borrow().is_empty() {
875 _ = conn.process(Duration::from_millis(500));
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),
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")
891 .context("Failed to get full path to current executable")?
895 r#"This program is not meant to be called from outside of switch-to-configuration."#
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
907 move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
910 if signal.unit.as_str() == "nixos-activation.service" {
911 *_nixos_activation_done.borrow_mut() = true;
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
921 _ = systemd.reexecute();
924 .restart_unit("nixos-activation.service", "replace")
925 .context("Failed to restart nixos-activation.service")?;
927 while !*nixos_activation_done.borrow() {
929 .process(Duration::from_secs(500))
930 .context("Failed to process dbus messages")?;
934 .remove_match(jobs_token)
935 .context("Failed to remove jobs token")?;
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 {
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
961 .split(std::path::MAIN_SEPARATOR_STR)
963 .unwrap_or("switch-to-configuration")
965 std::process::exit(1);
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);
979 let current_system_bin = std::path::PathBuf::from("/run/current-system/sw/bin")
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()
992 .map(|id| distro_id_re.is_match(id))
995 eprintln!("This is not a NixOS installation!");
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()
1007 .open("/run/nixos/switch-to-configuration.lock")
1009 eprintln!("Could not open lock");
1013 let Ok(_lock) = Flock::lock(lock, FlockArg::LockExclusive) else {
1014 eprintln!("Could not acquire lock");
1018 if syslog::init(Facility::LOG_USER, LevelFilter::Debug, Some("nixos")).is_err() {
1019 bail!("Failed to initialize logger");
1022 // Install or update the bootloader.
1023 if matches!(action, Action::Switch | Action::Boot) {
1024 do_install_bootloader(&install_bootloader, &toplevel)?;
1027 // Just in case the new configuration hangs the system, do a sync now.
1028 if std::env::var("NIXOS_NO_SYNC")
1030 .unwrap_or_default()
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")?;
1038 if *action == Action::Boot {
1039 std::process::exit(0);
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 {
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.
1057 std::process::exit(100);
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));
1083 .context("Failed to subscribe to systemd dbus messages")?;
1085 // Wait for the system to have finished booting.
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,
1095 .process(Duration::from_millis(500))
1096 .context("Failed to process dbus messages")?
1101 let _systemd_reload_status = systemd_reload_status.clone();
1102 let reloading_token = systemd
1104 move |signal: OrgFreedesktopSystemd1ManagerReloading,
1105 _: &LocalConnection,
1107 *_systemd_reload_status.borrow_mut() = signal.active;
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
1118 move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
1119 _: &LocalConnection,
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));
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 ¤t_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| {
1150 captures.get(1).map(|c| c.as_str()),
1151 captures.get(2).map(|c| c.as_str()),
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);
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()))
1167 base_name = new_base_name;
1170 if current_base_unit_file.exists()
1171 && (unit_state.state == "active" || unit_state.state == "activating")
1173 if new_base_unit_file
1175 .map(|full_path| full_path == Path::new("/dev/null"))
1178 let current_unit_info = parse_unit(¤t_unit_file, ¤t_base_unit_file)?;
1179 if parse_systemd_bool(Some(¤t_unit_info), "Unit", "X-StopOnRemoval", true) {
1180 _ = units_to_stop.insert(unit.to_string(), ());
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.
1191 "suspend.target" | "hibernate.target" | "hybrid-sleep.target"
1193 if !(parse_systemd_bool(
1194 Some(&new_unit_info),
1196 "RefuseManualStart",
1198 ) || parse_systemd_bool(
1199 Some(&new_unit_info),
1201 "X-OnlyManualStart",
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(), ());
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),
1224 "X-StopOnReconfiguration",
1227 units_to_stop.insert(unit.to_string(), ());
1230 let current_unit_info = parse_unit(¤t_unit_file, ¤t_base_unit_file)?;
1231 let new_unit_info = parse_unit(&new_unit_file, &new_base_unit_file)?;
1232 match compare_units(¤t_unit_info, &new_unit_info) {
1233 UnitComparison::UnequalNeedsRestart => {
1234 handle_modified_unit(
1239 &new_base_unit_file,
1240 Some(&new_unit_info),
1241 ¤t_active_units,
1243 &mut units_to_start,
1244 &mut units_to_reload,
1245 &mut units_to_restart,
1249 UnitComparison::UnequalNeedsReload if !units_to_restart.contains_key(unit) => {
1250 units_to_reload.insert(unit.clone(), ());
1251 record_unit(RELOAD_LIST_FILE, &unit);
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(¤t_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
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)
1283 // Don't unmount / or /nix if the device changed
1284 units_to_skip.insert(unit, ());
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);
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)
1297 // Filesystem entry disappeared, so unmount it.
1298 units_to_stop.insert(unit, ());
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);
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}");
1319 // FIXME: update swap options (i.e. its priority).
1322 // Should we have systemd re-exec itself?
1323 let current_pid1_path = Path::new("/proc/1/exe")
1325 .unwrap_or_else(|_| PathBuf::from("/unknown"));
1326 let current_systemd_system_config = Path::new("/etc/systemd/system.conf")
1328 .unwrap_or_else(|_| PathBuf::from("/unknown"));
1329 let Ok(new_pid1_path) = new_systemd.join("lib/systemd/systemd").canonicalize() else {
1332 let new_systemd_system_config = toplevel
1333 .join("etc/systemd/system.conf")
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
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(", "));
1354 if !units_to_skip.is_empty() {
1355 let mut units = units_to_skip
1358 .map(String::as_str)
1359 .collect::<Vec<&str>>();
1360 units.sort_by_key(|name| name.to_lowercase());
1362 "would NOT stop the following changed units: {}",
1367 eprintln!("would activate the configuration...");
1368 _ = std::process::Command::new(out.join("dry-activate"))
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()
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| {
1387 captures.get(1).map(|c| c.as_str()),
1388 captures.get(2).map(|c| c.as_str()),
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);
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()))
1403 base_name = new_base_name;
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(), ());
1412 handle_modified_unit(
1417 &new_base_unit_file,
1419 ¤t_active_units,
1421 &mut units_to_start,
1422 &mut units_to_reload,
1423 &mut units_to_restart,
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()
1435 if current_active_units.contains_key(unit)
1436 && !units_to_restart.contains_key(unit)
1437 && !units_to_stop.contains_key(unit)
1439 units_to_reload.insert(unit.to_string(), ());
1440 record_unit(RELOAD_LIST_FILE, unit);
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");
1451 if !units_to_reload.is_empty() {
1452 let mut units = units_to_reload
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(", "));
1461 if !units_to_restart.is_empty() {
1462 let mut units = units_to_restart
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(", "));
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
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(", "));
1482 std::process::exit(0);
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
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(", "));
1498 for unit in units_to_stop.keys() {
1499 match systemd.stop_unit(unit, "replace") {
1501 let mut j = submitted_jobs.borrow_mut();
1502 j.insert(job_path.to_owned(), Job::Stop);
1508 block_on_jobs(&dbus_conn, &submitted_jobs);
1511 if !units_to_skip.is_empty() {
1512 let mut units = units_to_skip
1515 .map(String::as_str)
1516 .collect::<Vec<&str>>();
1517 units.sort_by_key(|name| name.to_lowercase());
1519 "NOT restarting the following changed units: {}",
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"))
1534 .map(|mut child| child.wait())
1536 Ok(Ok(status)) if status.success() => {}
1538 // allow toplevel to not have an activation script
1541 eprintln!("Failed to run activate script");
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()
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| {
1559 captures.get(1).map(|c| c.as_str()),
1560 captures.get(2).map(|c| c.as_str()),
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);
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()))
1575 base_name = new_base_name;
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);
1585 handle_modified_unit(
1590 &new_base_unit_file,
1592 ¤t_active_units,
1594 &mut units_to_start,
1595 &mut units_to_reload,
1596 &mut units_to_restart,
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()
1609 if current_active_units.contains_key(unit)
1610 && !units_to_restart.contains_key(unit)
1611 && !units_to_stop.contains_key(unit)
1613 units_to_reload.insert(unit.to_string(), ());
1614 record_unit(RELOAD_LIST_FILE, unit);
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() {
1630 .process(Duration::from_millis(500))
1631 .context("Failed to process dbus messages")?;
1635 // Forget about previously failed services.
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() {
1644 .process(Duration::from_millis(500))
1645 .context("Failed to process dbus messages")?;
1649 .remove_match(reloading_token)
1650 .context("Failed to cleanup systemd Reloading match")?;
1652 // Reload user units
1653 match logind.list_users() {
1655 eprintln!("Unable to list users with logind: {err}");
1659 for (uid, name, user_dbus_path) in users {
1660 let gid: u32 = dbus_conn
1662 "org.freedesktop.login1",
1664 Duration::from_millis(5000),
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")
1672 .context("Failed to get full path to /proc/self/exe")?;
1674 std::process::Command::new(&myself)
1677 .env("XDG_RUNTIME_DIR", format!("/run/user/{}", uid))
1678 .env("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE", &myself)
1680 .with_context(|| format!("Failed to spawn user activation for {name}"))?
1682 .with_context(|| format!("Failed to run user activation for {name}"))?;
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") {
1696 let mut jobs = submitted_jobs.borrow_mut();
1697 jobs.insert(job_path, Job::Restart);
1700 eprintln!("Failed to restart {SYSINIT_REACTIVATION_TARGET}: {err}");
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(),
1719 if !parse_systemd_bool(Some(&unit_info), "Unit", "RefuseManualStart", false)
1720 || parse_systemd_bool(Some(&unit_info), "Unit", "X-OnlyManualStart", false)
1722 units_to_start.insert(unit.clone(), ());
1723 record_unit(START_LIST_FILE, &unit);
1725 // Don't reload the unit, reloading would fail
1726 units_to_reload.remove(&unit);
1727 unrecord_unit(RELOAD_LIST_FILE, &unit);
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
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(", "));
1743 match systemd.reload_unit(unit, "replace") {
1747 .insert(job_path.clone(), Job::Reload);
1750 eprintln!("Failed to reload {unit}: {err}");
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))?;
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
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(", "));
1773 match systemd.restart_unit(unit, "replace") {
1775 let mut jobs = submitted_jobs.borrow_mut();
1776 jobs.insert(job_path, Job::Restart);
1779 eprintln!("Failed to restart {unit}: {err}");
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))?;
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
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(", "));
1806 for unit in units_to_start.keys() {
1807 match systemd.start_unit(unit, "replace") {
1809 let mut jobs = submitted_jobs.borrow_mut();
1810 jobs.insert(job_path, Job::Start);
1813 eprintln!("Failed to start {unit}: {err}");
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);
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
1849 // Wait for events from systemd to settle. process() will return true if we have received any
1850 // messages on the bus.
1852 .process(Duration::from_millis(250))
1853 .unwrap_or_default()
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);
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
1868 .with_context(|| format!("Failed to get unit info for {unit}"))?;
1869 let exec_main_status: i32 = dbus_conn
1871 "org.freedesktop.systemd1",
1873 Duration::from_millis(5000),
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);
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")
1891 new_units.push(unit);
1895 if !new_units.is_empty() {
1896 new_units.sort_by_key(|name| name.to_lowercase());
1898 "the following new units were started: {}",
1899 new_units.join(", ")
1903 if !failed_units.is_empty() {
1904 failed_units.sort_by_key(|name| name.to_lowercase());
1906 "warning: the following units failed: {}",
1907 failed_units.join(", ")
1909 _ = std::process::Command::new(new_systemd.join("bin/systemctl"))
1915 .map(|mut child| child.wait());
1922 "finished switching to system configuration {}",
1927 "switching to system configuration {} failed (status {})",
1933 std::process::exit(exit_code);
1936 fn main() -> anyhow::Result<()> {
1938 unsafe { nix::libc::geteuid() },
1939 std::env::var("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE").ok(),
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),
1949 use std::collections::HashMap;
1954 let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(""));
1955 assert!(filesystems.is_empty());
1956 assert!(swaps.is_empty());
1960 let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
1965 assert!(filesystems.is_empty());
1966 assert!(swaps.is_empty());
1970 let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
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>
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
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");
2001 super::filter_units(&HashMap::from([]), &HashMap::from([])),
2006 super::filter_units(
2007 &HashMap::from([("foo".to_string(), ())]),
2008 &HashMap::from([("foo".to_string(), ()), ("bar".to_string(), ())])
2010 HashMap::from([("bar".to_string(), ())])
2015 fn compare_units() {
2018 super::compare_units(&HashMap::from([]), &HashMap::from([]))
2019 == super::UnitComparison::Equal
2023 super::compare_units(
2024 &HashMap::from([("Unit".to_string(), HashMap::from([]))]),
2026 ) == super::UnitComparison::Equal
2030 super::compare_units(
2034 "X-Reload-Triggers".to_string(),
2035 vec!["foobar".to_string()]
2039 ) == super::UnitComparison::Equal
2045 super::compare_units(
2046 &HashMap::from([("foobar".to_string(), HashMap::from([]))]),
2048 ) == super::UnitComparison::UnequalNeedsRestart
2052 super::compare_units(
2054 "Mount".to_string(),
2055 HashMap::from([("Options".to_string(), vec![])])
2058 "Mount".to_string(),
2059 HashMap::from([("Options".to_string(), vec!["ro".to_string()])])
2061 ) == super::UnitComparison::UnequalNeedsReload
2067 super::compare_units(
2072 "X-Reload-Triggers".to_string(),
2073 vec!["foobar".to_string()]
2076 ) == super::UnitComparison::UnequalNeedsReload
2080 super::compare_units(
2084 "X-Reload-Triggers".to_string(),
2085 vec!["foobar".to_string()]
2091 "X-Reload-Triggers".to_string(),
2092 vec!["barfoo".to_string()]
2095 ) == super::UnitComparison::UnequalNeedsReload
2099 super::compare_units(
2101 "Mount".to_string(),
2102 HashMap::from([("Type".to_string(), vec!["ext4".to_string()])])
2105 "Mount".to_string(),
2106 HashMap::from([("Type".to_string(), vec!["btrfs".to_string()])])
2108 ) == super::UnitComparison::UnequalNeedsRestart
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
2118 let mut unit_info = HashMap::new();
2120 let test_unit = std::io::Cursor::new(
2122 After=dev-disk-by\x2dlabel-root.device
2125 super::parse_systemd_ini(&mut unit_info, test_unit).unwrap();
2135 "dev-disk-by\\x2dlabel-root.device"