···
+
os::unix::{fs::PermissionsExt, process::CommandExt},
+
use anyhow::{anyhow, bail, Context, Result};
+
blocking::{stdintf::org_freedesktop_dbus::Properties, LocalConnection, Proxy},
+
fcntl::{Flock, FlockArg, OFlag},
+
signal::{self, SigHandler, Signal},
+
#![allow(non_upper_case_globals)]
+
#![allow(non_camel_case_types)]
+
#![allow(non_snake_case)]
+
include!(concat!(env!("OUT_DIR"), "/systemd_manager.rs"));
+
#![allow(non_upper_case_globals)]
+
#![allow(non_camel_case_types)]
+
#![allow(non_snake_case)]
+
include!(concat!(env!("OUT_DIR"), "/logind_manager.rs"));
+
use crate::systemd_manager::OrgFreedesktopSystemd1Manager;
+
logind_manager::OrgFreedesktopLogin1Manager,
+
OrgFreedesktopSystemd1ManagerJobRemoved, OrgFreedesktopSystemd1ManagerReloading,
+
type UnitInfo = HashMap<String, HashMap<String, Vec<String>>>;
+
const SYSINIT_REACTIVATION_TARGET: &str = "sysinit-reactivation.target";
+
// To be robust against interruption, record what units need to be started etc. We read these files
+
// again every time this program starts to make sure we continue where the old (interrupted) script
+
const START_LIST_FILE: &str = "/run/nixos/start-list";
+
const RESTART_LIST_FILE: &str = "/run/nixos/restart-list";
+
const RELOAD_LIST_FILE: &str = "/run/nixos/reload-list";
+
// Parse restart/reload requests by the activation script. Activation scripts may write
+
// newline-separated units to the restart file and switch-to-configuration will handle them. While
+
// `stopIfChanged = true` is ignored, switch-to-configuration will handle `restartIfChanged =
+
// false` and `reloadIfChanged = true`. This is the same as specifying a restart trigger in the
+
// The reload file asks this program to reload a unit. This is the same as specifying a reload
+
// trigger in the NixOS module and can be ignored if the unit is restarted in this activation.
+
const RESTART_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/activation-restart-list";
+
const RELOAD_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/activation-reload-list";
+
const DRY_RESTART_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/dry-activation-restart-list";
+
const DRY_RELOAD_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/dry-activation-reload-list";
+
#[derive(Debug, Clone, PartialEq)]
+
impl std::str::FromStr for Action {
+
type Err = anyhow::Error;
+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+
"switch" => Self::Switch,
+
"dry-activate" => Self::DryActivate,
+
_ => bail!("invalid action {s}"),
+
impl Into<&'static str> for &Action {
+
fn into(self) -> &'static str {
+
Action::Switch => "switch",
+
Action::Boot => "boot",
+
Action::Test => "test",
+
Action::DryActivate => "dry-activate",
+
// Allow for this switch-to-configuration to remain consistent with the perl implementation.
+
// Perl's "die" uses errno to set the exit code: https://perldoc.perl.org/perlvar#%24%21
+
std::process::exit(std::io::Error::last_os_error().raw_os_error().unwrap_or(1));
+
fn parse_os_release() -> Result<HashMap<String, String>> {
+
Ok(std::fs::read_to_string("/etc/os-release")
+
.context("Failed to read /etc/os-release")?
+
.fold(HashMap::new(), |mut acc, line| {
+
if let Some((k, v)) = line.split_once('=') {
+
acc.insert(k.to_string(), v.to_string());
+
fn do_install_bootloader(command: &str, toplevel: &Path) -> Result<()> {
+
let mut cmd_split = command.split_whitespace();
+
let Some(argv0) = cmd_split.next() else {
+
bail!("missing first argument in install bootloader commands");
+
match std::process::Command::new(argv0)
+
.args(cmd_split.collect::<Vec<&str>>())
+
.map(|mut child| child.wait())
+
Ok(Ok(status)) if status.success() => {}
+
eprintln!("Failed to install bootloader");
+
extern "C" fn handle_sigpipe(_signal: nix::libc::c_int) {}
+
fn required_env(var: &str) -> anyhow::Result<String> {
+
std::env::var(var).with_context(|| format!("missing required environment variable ${var}"))
+
// Asks the currently running systemd instance via dbus which units are active. Returns a hash
+
// where the key is the name of each unit and the value a hash of load, state, substate.
+
fn get_active_units<'a>(
+
systemd_manager: &Proxy<'a, &LocalConnection>,
+
) -> Result<HashMap<String, UnitState>> {
+
let units = systemd_manager
+
.list_units_by_patterns(Vec::new(), Vec::new())
+
.context("Failed to list systemd units")?;
+
if following == "" && active_state != "inactive" {
+
Some((id, active_state, sub_state))
+
.fold(HashMap::new(), |mut acc, (id, active_state, sub_state)| {
+
// This function takes a single ini file that specified systemd configuration like unit
+
// configuration and parses it into a HashMap where the keys are the sections of the unit file and
+
// the values are HashMaps themselves. These HashMaps have the unit file keys as their keys (left
+
// side of =) and an array of all values that were set as their values. If a value is empty (for
+
// example `ExecStart=`), then all current definitions are removed.
+
// Instead of returning the HashMap, this function takes a mutable reference to a HashMap to return
+
// the data in. This allows calling the function multiple times with the same Hashmap to parse
+
fn parse_systemd_ini(data: &mut UnitInfo, unit_file: &Path) -> Result<()> {
+
let ini = Ini::load_from_file(unit_file)
+
.with_context(|| format!("Failed to load unit file {}", unit_file.display()))?;
+
// Copy over all sections
+
for (section, properties) in ini.iter() {
+
let Some(section) = section else {
+
if section == "Install" {
+
// Skip the [Install] section because it has no relevant keys for us
+
let section_map = if let Some(section_map) = data.get_mut(section) {
+
data.insert(section.to_string(), HashMap::new());
+
.ok_or(anyhow!("section name should exist in hashmap"))?
+
for (ini_key, _) in properties {
+
let values = properties.get_all(ini_key);
+
.collect::<Vec<String>>();
+
let mut new_vals = Vec::new();
+
let mut clear_existing = false;
+
// If a value is empty, it's an override that tells us to clean the value
+
match (section_map.get_mut(ini_key), clear_existing) {
+
(Some(existing_vals), false) => existing_vals.extend(new_vals),
+
_ => _ = section_map.insert(ini_key.to_string(), new_vals),
+
// This function takes the path to a systemd configuration file (like a unit configuration) and
+
// parses it into a UnitInfo structure.
+
// If a directory with the same basename ending in .d exists next to the unit file, it will be
+
// assumed to contain override files which will be parsed as well and handled properly.
+
fn parse_unit(unit_file: &Path, base_unit_file: &Path) -> Result<UnitInfo> {
+
// Parse the main unit and all overrides
+
let mut unit_data = HashMap::new();
+
parse_systemd_ini(&mut unit_data, base_unit_file)?;
+
glob(&format!("{}.d/*.conf", base_unit_file.display())).context("Invalid glob pattern")?
+
let Ok(entry) = entry else {
+
parse_systemd_ini(&mut unit_data, &entry)?;
+
// Handle drop-in template-unit instance overrides
+
if unit_file != base_unit_file {
+
glob(&format!("{}.d/*.conf", unit_file.display())).context("Invalid glob pattern")?
+
let Ok(entry) = entry else {
+
parse_systemd_ini(&mut unit_data, &entry)?;
+
// Checks whether a specified boolean in a systemd unit is true or false, with a default that is
+
// applied when the value is not set.
+
unit_data: Option<&UnitInfo>,
+
if let Some(Some(Some(Some(b)))) = unit_data.map(|data| {
+
data.get(section_name).map(|section| {
+
section.get(bool_name).map(|vals| {
+
.map(|last| matches!(last.as_str(), "1" | "yes" | "true" | "on"))
+
#[derive(Debug, PartialEq)]
+
// Compare the contents of two unit files and return whether the unit needs to be restarted or
+
// reloaded. If the units differ, the service is restarted unless the only difference is
+
// `X-Reload-Triggers` in the `Unit` section. If this is the only modification, the unit is
+
// reloaded instead of restarted. If the only difference is `Options` in the `[Mount]` section, the
+
// unit is reloaded rather than restarted.
+
fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison {
+
let mut ret = UnitComparison::Equal;
+
let unit_section_ignores = HashMap::from(
+
.map(|name| (name, ())),
+
let mut section_cmp = new_unit.keys().fold(HashMap::new(), |mut acc, key| {
+
acc.insert(key.as_str(), ());
+
// Iterate over the sections
+
for (section_name, section_val) in current_unit {
+
// Missing section in the new unit?
+
if !section_cmp.contains_key(section_name.as_str()) {
+
// If the [Unit] section was removed, make sure that only keys were in it that are
+
if section_name == "Unit" {
+
for (ini_key, _ini_val) in section_val {
+
if !unit_section_ignores.contains_key(ini_key.as_str()) {
+
return UnitComparison::UnequalNeedsRestart;
+
continue; // check the next section
+
return UnitComparison::UnequalNeedsRestart;
+
section_cmp.remove(section_name.as_str());
+
// Comparison hash for the section contents
+
let mut ini_cmp = new_unit
+
section_val.keys().fold(HashMap::new(), |mut acc, ini_key| {
+
acc.insert(ini_key.as_str(), ());
+
// Iterate over the keys of the section
+
for (ini_key, current_value) in section_val {
+
ini_cmp.remove(ini_key.as_str());
+
let Some(Some(new_value)) = new_unit
+
.map(|section| section.get(ini_key))
+
// If the key is missing in the new unit, they are different unless the key that is
+
// now missing is one of the ignored keys
+
if section_name == "Unit" && unit_section_ignores.contains_key(ini_key.as_str()) {
+
return UnitComparison::UnequalNeedsRestart;
+
// If the contents are different, the units are different
+
if current_value != new_value {
+
if section_name == "Unit" {
+
if ini_key == "X-Reload-Triggers" {
+
ret = UnitComparison::UnequalNeedsReload;
+
} else if unit_section_ignores.contains_key(ini_key.as_str()) {
+
// If this is a mount unit, check if it was only `Options`
+
if section_name == "Mount" && ini_key == "Options" {
+
ret = UnitComparison::UnequalNeedsReload;
+
return UnitComparison::UnequalNeedsRestart;
+
// A key was introduced that was missing in the previous unit
+
if !ini_cmp.is_empty() {
+
if section_name == "Unit" {
+
for (ini_key, _) in ini_cmp {
+
if ini_key == "X-Reload-Triggers" {
+
ret = UnitComparison::UnequalNeedsReload;
+
} else if unit_section_ignores.contains_key(ini_key) {
+
return UnitComparison::UnequalNeedsRestart;
+
return UnitComparison::UnequalNeedsRestart;
+
// A section was introduced that was missing in the previous unit
+
if !section_cmp.is_empty() {
+
if section_cmp.keys().len() == 1 && section_cmp.contains_key("Unit") {
+
if let Some(new_unit_unit) = new_unit.get("Unit") {
+
for (ini_key, _) in new_unit_unit {
+
if !unit_section_ignores.contains_key(ini_key.as_str()) {
+
return UnitComparison::UnequalNeedsRestart;
+
} else if ini_key == "X-Reload-Triggers" {
+
ret = UnitComparison::UnequalNeedsReload;
+
return UnitComparison::UnequalNeedsRestart;
+
// Called when a unit exists in both the old systemd and the new system and the units differ. This
+
// figures out of what units are to be stopped, restarted, reloaded, started, and skipped.
+
fn handle_modified_unit(
+
new_base_unit_file: &Path,
+
new_unit_info: Option<&UnitInfo>,
+
active_cur: &HashMap<String, UnitState>,
+
units_to_stop: &mut HashMap<String, ()>,
+
units_to_start: &mut HashMap<String, ()>,
+
units_to_reload: &mut HashMap<String, ()>,
+
units_to_restart: &mut HashMap<String, ()>,
+
units_to_skip: &mut HashMap<String, ()>,
+
let use_restart_as_stop_and_start = new_unit_info.is_none();
+
"sysinit.target" | "basic.target" | "multi-user.target" | "graphical.target"
+
) || unit.ends_with(".unit")
+
|| unit.ends_with(".slice")
+
// Do nothing. These cannot be restarted directly.
+
// Slices and Paths don't have to be restarted since properties (resource limits and
+
// inotify watches) seem to get applied on daemon-reload.
+
} else if unit.ends_with(".mount") {
+
// Just restart the unit. We wouldn't have gotten into this subroutine if only `Options`
+
// was changed, in which case the unit would be reloaded. The only exception is / and /nix
+
// because it's very unlikely we can safely unmount them so we reload them instead. This
+
// means that we may not get all changes into the running system but it's better than
+
if unit == "-.mount" || unit == "nix.mount" {
+
units_to_reload.insert(unit.to_string(), ());
+
record_unit(RELOAD_LIST_FILE, unit);
+
units_to_restart.insert(unit.to_string(), ());
+
record_unit(RESTART_LIST_FILE, unit);
+
} else if unit.ends_with(".socket") {
+
// FIXME: do something?
+
// Attempt to fix this: https://github.com/NixOS/nixpkgs/pull/141192
+
// Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
+
// More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
+
let fallback = parse_unit(new_unit_file, new_base_unit_file)?;
+
let new_unit_info = if new_unit_info.is_some() {
+
if parse_systemd_bool(new_unit_info, "Service", "X-ReloadIfChanged", false)
+
&& !units_to_restart.contains_key(unit)
+
&& !(if use_restart_as_stop_and_start {
+
units_to_restart.contains_key(unit)
+
units_to_stop.contains_key(unit)
+
units_to_reload.insert(unit.to_string(), ());
+
record_unit(RELOAD_LIST_FILE, unit);
+
} else if !parse_systemd_bool(new_unit_info, "Service", "X-RestartIfChanged", true)
+
|| parse_systemd_bool(new_unit_info, "Unit", "RefuseManualStop", false)
+
|| parse_systemd_bool(new_unit_info, "Unit", "X-OnlyManualStart", false)
+
units_to_skip.insert(unit.to_string(), ());
+
// It doesn't make sense to stop and start non-services because they can't have
+
if !parse_systemd_bool(new_unit_info, "Service", "X-StopIfChanged", true)
+
|| !unit.ends_with(".service")
+
// This unit should be restarted instead of stopped and started.
+
units_to_restart.insert(unit.to_string(), ());
+
record_unit(RESTART_LIST_FILE, unit);
+
// Remove from units to reload so we don't restart and reload
+
if units_to_reload.contains_key(unit) {
+
units_to_reload.remove(unit);
+
unrecord_unit(RELOAD_LIST_FILE, unit);
+
// If this unit is socket-activated, then stop the socket unit(s) as well, and
+
// restart the socket(s) instead of the service.
+
let mut socket_activated = false;
+
if unit.ends_with(".service") {
+
let mut sockets = if let Some(Some(Some(sockets))) = new_unit_info.map(|info| {
+
.map(|service_section| service_section.get("Sockets"))
+
if sockets.is_empty() {
+
sockets.push(format!("{}.socket", base_name));
+
for socket in &sockets {
+
if active_cur.contains_key(socket) {
+
// We can now be sure this is a socket-activated unit
+
if use_restart_as_stop_and_start {
+
units_to_restart.insert(socket.to_string(), ());
+
units_to_stop.insert(socket.to_string(), ());
+
// Only restart sockets that actually exist in new configuration:
+
if toplevel.join("etc/systemd/system").join(socket).exists() {
+
if use_restart_as_stop_and_start {
+
units_to_restart.insert(socket.to_string(), ());
+
record_unit(RESTART_LIST_FILE, socket);
+
units_to_start.insert(socket.to_string(), ());
+
record_unit(START_LIST_FILE, socket);
+
socket_activated = true;
+
// Remove from units to reload so we don't restart and reload
+
if units_to_reload.contains_key(unit) {
+
units_to_reload.remove(unit);
+
unrecord_unit(RELOAD_LIST_FILE, unit);
+
// If the unit is not socket-activated, record that this unit needs to be started
+
// below. We write this to a file to ensure that the service gets restarted if
+
if use_restart_as_stop_and_start {
+
units_to_restart.insert(unit.to_string(), ());
+
record_unit(RESTART_LIST_FILE, unit);
+
units_to_start.insert(unit.to_string(), ());
+
record_unit(START_LIST_FILE, unit);
+
if use_restart_as_stop_and_start {
+
units_to_restart.insert(unit.to_string(), ());
+
units_to_stop.insert(unit.to_string(), ());
+
// Remove from units to reload so we don't restart and reload
+
if units_to_reload.contains_key(unit) {
+
units_to_reload.remove(unit);
+
unrecord_unit(RELOAD_LIST_FILE, unit);
+
// Writes a unit name into a given file to be more resilient against crashes of the script. Does
+
// nothing when the action is dry-activate.
+
fn record_unit(p: impl AsRef<Path>, unit: &str) {
+
if ACTION.get() != Some(&Action::DryActivate) {
+
if let Ok(mut f) = std::fs::File::options().append(true).create(true).open(p) {
+
_ = writeln!(&mut f, "{unit}");
+
// The opposite of record_unit, removes a unit name from a file
+
fn unrecord_unit(p: impl AsRef<Path>, unit: &str) {
+
if ACTION.get() != Some(&Action::DryActivate) {
+
if let Ok(contents) = std::fs::read_to_string(&p) {
+
if let Ok(mut f) = std::fs::File::options()
+
.filter(|line| line != &unit)
+
.for_each(|line| _ = writeln!(&mut f, "{line}"))
+
fn map_from_list_file(p: impl AsRef<Path>) -> HashMap<String, ()> {
+
std::fs::read_to_string(p)
+
.filter(|line| !line.is_empty())
+
.fold(HashMap::new(), |mut acc, line| {
+
acc.insert(line.to_string(), ());
+
// Parse a fstab file, given its path. Returns a tuple of filesystems and swaps.
+
// Filesystems is a hash of mountpoint and { device, fsType, options } Swaps is a hash of device
+
fn parse_fstab(fstab: impl BufRead) -> (HashMap<String, Filesystem>, HashMap<String, Swap>) {
+
let mut filesystems = HashMap::new();
+
let mut swaps = HashMap::new();
+
for line in fstab.lines() {
+
let Ok(line) = line else {
+
if line.contains('#') {
+
let mut split = line.split_whitespace();
+
let (Some(device), Some(mountpoint), Some(fs_type), options) = (
+
split.next().unwrap_or_default(),
+
swaps.insert(device.to_string(), Swap(options.to_string()));
+
mountpoint.to_string(),
+
device: device.to_string(),
+
fs_type: fs_type.to_string(),
+
options: options.to_string(),
+
// Converts a path to the name of a systemd mount unit that would be responsible for mounting this
+
fn path_to_unit_name(bin_path: &Path, path: &str) -> String {
+
let Ok(output) = std::process::Command::new(bin_path.join("systemd-escape"))
+
eprintln!("Unable to escape {}!", path);
+
let Ok(unit) = String::from_utf8(output.stdout) else {
+
eprintln!("Unable to convert systemd-espape output to valid UTF-8");
+
unit.trim().to_string()
+
// Returns a HashMap containing the same contents as the passed in `units`, minus the units in
+
units_to_filter: &HashMap<String, ()>,
+
units: &HashMap<String, ()>,
+
) -> HashMap<String, ()> {
+
let mut res = HashMap::new();
+
for (unit, _) in units {
+
if !units_to_filter.contains_key(unit) {
+
res.insert(unit.to_string(), ());
+
fn unit_is_active<'a>(conn: &LocalConnection, unit: &str) -> Result<bool> {
+
let unit_object_path = conn
+
"org.freedesktop.systemd1",
+
"/org/freedesktop/systemd1",
+
Duration::from_millis(5000),
+
.with_context(|| format!("Failed to get unit {unit}"))?;
+
let active_state: String = conn
+
"org.freedesktop.systemd1",
+
Duration::from_millis(5000),
+
.get("org.freedesktop.systemd1.Unit", "ActiveState")
+
.with_context(|| format!("Failed to get ExecMainStatus for {unit}"))?;
+
Ok(matches!(active_state.as_str(), "active" | "activating"))
+
static ACTION: OnceLock<Action> = OnceLock::new();
+
impl std::fmt::Display for Job {
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
Job::Restart => "restart",
+
Job::Reload => "reload",
+
fn new_dbus_proxies<'a>(
+
conn: &'a LocalConnection,
+
Proxy<'a, &'a LocalConnection>,
+
Proxy<'a, &'a LocalConnection>,
+
"org.freedesktop.systemd1",
+
"/org/freedesktop/systemd1",
+
Duration::from_millis(5000),
+
"org.freedesktop.login1",
+
"/org/freedesktop/login1",
+
Duration::from_millis(5000),
+
conn: &LocalConnection,
+
submitted_jobs: &Rc<RefCell<HashMap<dbus::Path<'static>, Job>>>,
+
while !submitted_jobs.borrow().is_empty() {
+
_ = conn.process(Duration::from_millis(500));
+
fn remove_file_if_exists(p: impl AsRef<Path>) -> std::io::Result<()> {
+
match std::fs::remove_file(p) {
+
Err(err) if err.kind() != std::io::ErrorKind::NotFound => Err(err),
+
/// Performs switch-to-configuration functionality for a single non-root user
+
fn do_user_switch(parent_exe: String) -> anyhow::Result<()> {
+
if Path::new(&parent_exe)
+
!= Path::new("/proc/self/exe")
+
.context("Failed to get full path to current executable")?
+
r#"This program is not meant to be called from outside of switch-to-configuration."#
+
let dbus_conn = LocalConnection::new_session().context("Failed to open dbus connection")?;
+
let (systemd, _) = new_dbus_proxies(&dbus_conn);
+
let nixos_activation_done = Rc::new(RefCell::new(false));
+
let _nixos_activation_done = nixos_activation_done.clone();
+
let jobs_token = systemd
+
move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
+
if signal.unit.as_str() == "nixos-activation.service" {
+
*_nixos_activation_done.borrow_mut() = true;
+
.context("Failed to add signal match for systemd removed jobs")?;
+
// The systemd user session seems to not send a Reloaded signal, so we don't have anything to
+
_ = systemd.reexecute();
+
.restart_unit("nixos-activation.service", "replace")
+
.context("Failed to restart nixos-activation.service")?;
+
while !*nixos_activation_done.borrow() {
+
.process(Duration::from_secs(500))
+
.context("Failed to process dbus messages")?;
+
.remove_match(jobs_token)
+
.context("Failed to remove jobs token")?;
+
/// Performs switch-to-configuration functionality for the entire system
+
fn do_system_switch() -> anyhow::Result<()> {
+
let out = PathBuf::from(required_env("OUT")?);
+
let toplevel = PathBuf::from(required_env("TOPLEVEL")?);
+
let distro_id = required_env("DISTRO_ID")?;
+
let install_bootloader = required_env("INSTALL_BOOTLOADER")?;
+
let locale_archive = required_env("LOCALE_ARCHIVE")?;
+
let new_systemd = PathBuf::from(required_env("SYSTEMD")?);
+
let mut args = std::env::args();
+
let argv0 = args.next().ok_or(anyhow!("no argv[0]"))?;
+
let Some(Ok(action)) = args.next().map(|a| Action::from_str(&a)) else {
+
r#"Usage: {} [switch|boot|test|dry-activate]
+
switch: make the configuration the boot default and activate now
+
boot: make the configuration the boot default
+
test: activate the configuration, but don't make it the boot default
+
dry-activate: show what would be done if this configuration were activated
+
.split(std::path::MAIN_SEPARATOR_STR)
+
.unwrap_or("switch-to-configuration")
+
let action = ACTION.get_or_init(|| action);
+
// The action that is to be performed (like switch, boot, test, dry-activate) Also exposed via
+
// environment variable from now on
+
std::env::set_var("NIXOS_ACTION", Into::<&'static str>::into(action));
+
// Expose the locale archive as an environment variable for systemctl and the activation script
+
if !locale_archive.is_empty() {
+
std::env::set_var("LOCALE_ARCHIVE", locale_archive);
+
let current_system_bin = std::path::PathBuf::from("/run/current-system/sw/bin")
+
.context("/run/current-system/sw/bin is missing")?;
+
let os_release = parse_os_release().context("Failed to parse os-release")?;
+
let distro_id_re = Regex::new(format!("^\"?{}\"?$", distro_id).as_str())
+
.context("Invalid regex for distro ID")?;
+
// This is a NixOS installation if it has /etc/NIXOS or a proper /etc/os-release.
+
if !Path::new("/etc/NIXOS").is_file()
+
.map(|id| distro_id_re.is_match(id))
+
eprintln!("This is not a NixOS installation!");
+
std::fs::create_dir_all("/run/nixos").context("Failed to create /run/nixos directory")?;
+
let perms = std::fs::Permissions::from_mode(0o755);
+
std::fs::set_permissions("/run/nixos", perms)
+
.context("Failed to set permissions on /run/nixos directory")?;
+
let Ok(lock) = std::fs::OpenOptions::new()
+
.open("/run/nixos/switch-to-configuration.lock")
+
eprintln!("Could not open lock");
+
let Ok(_lock) = Flock::lock(lock, FlockArg::LockExclusive) else {
+
eprintln!("Could not acquire lock");
+
if syslog::init(Facility::LOG_USER, LevelFilter::Debug, Some("nixos")).is_err() {
+
bail!("Failed to initialize logger");
+
// Install or update the bootloader.
+
if matches!(action, Action::Switch | Action::Boot) {
+
do_install_bootloader(&install_bootloader, &toplevel)?;
+
// Just in case the new configuration hangs the system, do a sync now.
+
if std::env::var("NIXOS_NO_SYNC")
+
let fd = nix::fcntl::open("/nix/store", OFlag::O_NOCTTY, Mode::S_IROTH)
+
.context("Failed to open /nix/store")?;
+
nix::unistd::syncfs(fd).context("Failed to sync /nix/store")?;
+
if *action == Action::Boot {
+
let current_init_interface_version =
+
std::fs::read_to_string("/run/current-system/init-interface-version").unwrap_or_default();
+
let new_init_interface_version =
+
std::fs::read_to_string(toplevel.join("init-interface-version"))
+
.context("File init-interface-version should exist")?;
+
// Check if we can activate the new configuration.
+
if current_init_interface_version != new_init_interface_version {
+
r#"Warning: the new NixOS configuration has an ‘init’ that is
+
incompatible with the current configuration. The new configuration
+
won't take effect until you reboot the system.
+
std::process::exit(100);
+
// Ignore SIGHUP so that we're not killed if we're running on (say) virtual console 1 and we
+
// restart the "tty1" unit.
+
let handler = SigHandler::Handler(handle_sigpipe);
+
unsafe { signal::signal(Signal::SIGPIPE, handler) }.context("Failed to set SIGPIPE handler")?;
+
let mut units_to_stop = HashMap::new();
+
let mut units_to_skip = HashMap::new();
+
let mut units_to_filter = HashMap::new(); // units not shown
+
let mut units_to_start = map_from_list_file(START_LIST_FILE);
+
let mut units_to_restart = map_from_list_file(RESTART_LIST_FILE);
+
let mut units_to_reload = map_from_list_file(RELOAD_LIST_FILE);
+
let dbus_conn = LocalConnection::new_system().context("Failed to open dbus connection")?;
+
let (systemd, logind) = new_dbus_proxies(&dbus_conn);
+
let submitted_jobs = Rc::new(RefCell::new(HashMap::new()));
+
let finished_jobs = Rc::new(RefCell::new(HashMap::new()));
+
let systemd_reload_status = Rc::new(RefCell::new(false));
+
.context("Failed to subscribe to systemd dbus messages")?;
+
// Wait for the system to have finished booting.
+
let system_state: String = systemd
+
.get("org.freedesktop.systemd1.Manager", "SystemState")
+
.context("Failed to get system state")?;
+
match system_state.as_str() {
+
"running" | "degraded" | "maintenance" => break,
+
.process(Duration::from_millis(500))
+
.context("Failed to process dbus messages")?
+
let _systemd_reload_status = systemd_reload_status.clone();
+
let reloading_token = systemd
+
move |signal: OrgFreedesktopSystemd1ManagerReloading,
+
*_systemd_reload_status.borrow_mut() = signal.active;
+
.context("Failed to add systemd Reloading match")?;
+
let _submitted_jobs = submitted_jobs.clone();
+
let _finished_jobs = finished_jobs.clone();
+
let job_removed_token = systemd
+
move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
+
if let Some(old) = _submitted_jobs.borrow_mut().remove(&signal.job) {
+
let mut finished_jobs = _finished_jobs.borrow_mut();
+
finished_jobs.insert(signal.job, (signal.unit, old, signal.result));
+
.context("Failed to add systemd JobRemoved match")?;
+
let current_active_units = get_active_units(&systemd)?;
+
let template_unit_re = Regex::new(r"^(.*)@[^\.]*\.(.*)$")
+
.context("Invalid regex for matching systemd template units")?;
+
let unit_name_re = Regex::new(r"^(.*)\.[[:lower:]]*$")
+
.context("Invalid regex for matching systemd unit names")?;
+
for (unit, unit_state) in ¤t_active_units {
+
let current_unit_file = Path::new("/etc/systemd/system").join(&unit);
+
let new_unit_file = toplevel.join("etc/systemd/system").join(&unit);
+
let mut base_unit = unit.clone();
+
let mut current_base_unit_file = current_unit_file.clone();
+
let mut new_base_unit_file = new_unit_file.clone();
+
// Detect template instances
+
if let Some((Some(template_name), Some(template_instance))) =
+
template_unit_re.captures(&unit).map(|captures| {
+
captures.get(1).map(|c| c.as_str()),
+
captures.get(2).map(|c| c.as_str()),
+
if !current_unit_file.exists() && !new_unit_file.exists() {
+
base_unit = format!("{}@.{}", template_name, template_instance);
+
current_base_unit_file = Path::new("/etc/systemd/system").join(&base_unit);
+
new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
+
let mut base_name = base_unit.as_str();
+
if let Some(Some(new_base_name)) = unit_name_re
+
.map(|capture| capture.get(1).map(|first| first.as_str()))
+
base_name = new_base_name;
+
if current_base_unit_file.exists()
+
&& (unit_state.state == "active" || unit_state.state == "activating")
+
.map(|full_path| full_path == Path::new("/dev/null"))
+
let current_unit_info = parse_unit(¤t_unit_file, ¤t_base_unit_file)?;
+
if parse_systemd_bool(Some(¤t_unit_info), "Unit", "X-StopOnRemoval", true) {
+
_ = units_to_stop.insert(unit.to_string(), ());
+
} else if unit.ends_with(".target") {
+
let new_unit_info = parse_unit(&new_unit_file, &new_base_unit_file)?;
+
// Cause all active target units to be restarted below. This should start most
+
// changed units we stop here as well as any new dependencies (including new mounts
+
// and swap devices). FIXME: the suspend target is sometimes active after the
+
// system has resumed, which probably should not be the case. Just ignore it.
+
"suspend.target" | "hibernate.target" | "hybrid-sleep.target"
+
if !(parse_systemd_bool(
+
) || parse_systemd_bool(
+
units_to_start.insert(unit.to_string(), ());
+
record_unit(START_LIST_FILE, unit);
+
// Don't spam the user with target units that always get started.
+
if std::env::var("STC_DISPLAY_ALL_UNITS").as_deref() != Ok("1") {
+
units_to_filter.insert(unit.to_string(), ());
+
// Stop targets that have X-StopOnReconfiguration set. This is necessary to respect
+
// dependency orderings involving targets: if unit X starts after target Y and
+
// target Y starts after unit Z, then if X and Z have both changed, then X should
+
// be restarted after Z. However, if target Y is in the "active" state, X and Z
+
// will be restarted at the same time because X's dependency on Y is already
+
// satisfied. Thus, we need to stop Y first. Stopping a target generally has no
+
// effect on other units (unless there is a PartOf dependency), so this is just a
+
// bookkeeping thing to get systemd to do the right thing.
+
"X-StopOnReconfiguration",
+
units_to_stop.insert(unit.to_string(), ());
+
let current_unit_info = parse_unit(¤t_unit_file, ¤t_base_unit_file)?;
+
let new_unit_info = parse_unit(&new_unit_file, &new_base_unit_file)?;
+
match compare_units(¤t_unit_info, &new_unit_info) {
+
UnitComparison::UnequalNeedsRestart => {
+
UnitComparison::UnequalNeedsReload if !units_to_restart.contains_key(unit) => {
+
units_to_reload.insert(unit.clone(), ());
+
record_unit(RELOAD_LIST_FILE, &unit);
+
// Compare the previous and new fstab to figure out which filesystems need a remount or need to
+
// be unmounted. New filesystems are mounted automatically by starting local-fs.target.
+
// FIXME: might be nicer if we generated units for all mounts; then we could unify this with
+
// the unit checking code above.
+
let (current_filesystems, current_swaps) = std::fs::read_to_string("/etc/fstab")
+
.map(|fstab| parse_fstab(std::io::Cursor::new(fstab)))
+
let (new_filesystems, new_swaps) = std::fs::read_to_string(toplevel.join("etc/fstab"))
+
.map(|fstab| parse_fstab(std::io::Cursor::new(fstab)))
+
for (mountpoint, current_filesystem) in current_filesystems {
+
// Use current version of systemctl binary before daemon is reexeced.
+
let unit = path_to_unit_name(¤t_system_bin, &mountpoint);
+
if let Some(new_filesystem) = new_filesystems.get(&mountpoint) {
+
if current_filesystem.fs_type != new_filesystem.fs_type
+
|| current_filesystem.device != new_filesystem.device
+
if matches!(mountpoint.as_str(), "/" | "/nix") {
+
if current_filesystem.options != new_filesystem.options {
+
// Mount options changes, so remount it.
+
units_to_reload.insert(unit.to_string(), ());
+
record_unit(RELOAD_LIST_FILE, &unit)
+
// Don't unmount / or /nix if the device changed
+
units_to_skip.insert(unit, ());
+
// Filesystem type or device changed, so unmount and mount it.
+
units_to_restart.insert(unit.to_string(), ());
+
record_unit(RESTART_LIST_FILE, &unit);
+
} else if current_filesystem.options != new_filesystem.options {
+
// Mount options changes, so remount it.
+
units_to_reload.insert(unit.to_string(), ());
+
record_unit(RELOAD_LIST_FILE, &unit)
+
// Filesystem entry disappeared, so unmount it.
+
units_to_stop.insert(unit, ());
+
// Also handles swap devices.
+
for (device, _) in current_swaps {
+
if new_swaps.get(&device).is_none() {
+
// Swap entry disappeared, so turn it off. Can't use "systemctl stop" here because
+
// systemd has lots of alias units that prevent a stop from actually calling "swapoff".
+
if *action == Action::DryActivate {
+
eprintln!("would stop swap device: {}", &device);
+
eprintln!("stopping swap device: {}", &device);
+
let c_device = std::ffi::CString::new(device.clone())
+
.context("failed to convert device to cstring")?;
+
if unsafe { nix::libc::swapoff(c_device.as_ptr()) } != 0 {
+
let err = std::io::Error::last_os_error();
+
eprintln!("Failed to stop swapping to {device}: {err}");
+
// FIXME: update swap options (i.e. its priority).
+
// Should we have systemd re-exec itself?
+
let current_pid1_path = Path::new("/proc/1/exe")
+
.unwrap_or_else(|_| PathBuf::from("/unknown"));
+
let current_systemd_system_config = Path::new("/etc/systemd/system.conf")
+
.unwrap_or_else(|_| PathBuf::from("/unknown"));
+
let Ok(new_pid1_path) = new_systemd.join("lib/systemd/systemd").canonicalize() else {
+
let new_systemd_system_config = toplevel
+
.join("etc/systemd/system.conf")
+
.unwrap_or_else(|_| PathBuf::from("/unknown"));
+
let restart_systemd = current_pid1_path != new_pid1_path
+
|| current_systemd_system_config != new_systemd_system_config;
+
let units_to_stop_filtered = filter_units(&units_to_filter, &units_to_stop);
+
// Show dry-run actions.
+
if *action == Action::DryActivate {
+
if !units_to_stop_filtered.is_empty() {
+
let mut units = units_to_stop_filtered
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
eprintln!("would stop the following units: {}", units.join(", "));
+
if !units_to_skip.is_empty() {
+
let mut units = units_to_skip
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
"would NOT stop the following changed units: {}",
+
eprintln!("would activate the configuration...");
+
_ = std::process::Command::new(out.join("dry-activate"))
+
.map(|mut child| child.wait());
+
// Handle the activation script requesting the restart or reload of a unit.
+
for unit in std::fs::read_to_string(DRY_RESTART_BY_ACTIVATION_LIST_FILE)
+
let current_unit_file = Path::new("/etc/systemd/system").join(unit);
+
let new_unit_file = toplevel.join("etc/systemd/system").join(unit);
+
let mut base_unit = unit.to_string();
+
let mut new_base_unit_file = new_unit_file.clone();
+
// Detect template instances.
+
if let Some((Some(template_name), Some(template_instance))) =
+
template_unit_re.captures(&unit).map(|captures| {
+
captures.get(1).map(|c| c.as_str()),
+
captures.get(2).map(|c| c.as_str()),
+
if !current_unit_file.exists() && !new_unit_file.exists() {
+
base_unit = format!("{}@.{}", template_name, template_instance);
+
new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
+
let mut base_name = base_unit.as_str();
+
if let Some(Some(new_base_name)) = unit_name_re
+
.map(|capture| capture.get(1).map(|first| first.as_str()))
+
base_name = new_base_name;
+
// Start units if they were not active previously
+
if !current_active_units.contains_key(unit) {
+
units_to_start.insert(unit.to_string(), ());
+
remove_file_if_exists(DRY_RESTART_BY_ACTIVATION_LIST_FILE)
+
.with_context(|| format!("Failed to remove {}", DRY_RESTART_BY_ACTIVATION_LIST_FILE))?;
+
for unit in std::fs::read_to_string(DRY_RELOAD_BY_ACTIVATION_LIST_FILE)
+
if current_active_units.contains_key(unit)
+
&& !units_to_restart.contains_key(unit)
+
&& !units_to_stop.contains_key(unit)
+
units_to_reload.insert(unit.to_string(), ());
+
record_unit(RELOAD_LIST_FILE, unit);
+
remove_file_if_exists(DRY_RELOAD_BY_ACTIVATION_LIST_FILE)
+
.with_context(|| format!("Failed to remove {}", DRY_RELOAD_BY_ACTIVATION_LIST_FILE))?;
+
eprintln!("would restart systemd");
+
if !units_to_reload.is_empty() {
+
let mut units = units_to_reload
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
eprintln!("would reload the following units: {}", units.join(", "));
+
if !units_to_restart.is_empty() {
+
let mut units = units_to_restart
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
eprintln!("would restart the following units: {}", units.join(", "));
+
let units_to_start_filtered = filter_units(&units_to_filter, &units_to_start);
+
if !units_to_start_filtered.is_empty() {
+
let mut units = units_to_start_filtered
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
eprintln!("would start the following units: {}", units.join(", "));
+
log::info!("switching to system configuration {}", toplevel.display());
+
if !units_to_stop.is_empty() {
+
if !units_to_stop_filtered.is_empty() {
+
let mut units = units_to_stop_filtered
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
eprintln!("stopping the following units: {}", units.join(", "));
+
for unit in units_to_stop.keys() {
+
match systemd.stop_unit(unit, "replace") {
+
let mut j = submitted_jobs.borrow_mut();
+
j.insert(job_path.to_owned(), Job::Stop);
+
block_on_jobs(&dbus_conn, &submitted_jobs);
+
if !units_to_skip.is_empty() {
+
let mut units = units_to_skip
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
"NOT restarting the following changed units: {}",
+
// Wait for all stop jobs to finish
+
block_on_jobs(&dbus_conn, &submitted_jobs);
+
// Activate the new configuration (i.e., update /etc, make accounts, and so on).
+
eprintln!("activating the configuration...");
+
match std::process::Command::new(out.join("activate"))
+
.map(|mut child| child.wait())
+
Ok(Ok(status)) if status.success() => {}
+
// allow toplevel to not have an activation script
+
eprintln!("Failed to run activate script");
+
// Handle the activation script requesting the restart or reload of a unit.
+
for unit in std::fs::read_to_string(RESTART_BY_ACTIVATION_LIST_FILE)
+
let new_unit_file = toplevel.join("etc/systemd/system").join(unit);
+
let mut base_unit = unit.to_string();
+
let mut new_base_unit_file = new_unit_file.clone();
+
// Detect template instances.
+
if let Some((Some(template_name), Some(template_instance))) =
+
template_unit_re.captures(&unit).map(|captures| {
+
captures.get(1).map(|c| c.as_str()),
+
captures.get(2).map(|c| c.as_str()),
+
if !new_unit_file.exists() {
+
base_unit = format!("{}@.{}", template_name, template_instance);
+
new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
+
let mut base_name = base_unit.as_str();
+
if let Some(Some(new_base_name)) = unit_name_re
+
.map(|capture| capture.get(1).map(|first| first.as_str()))
+
base_name = new_base_name;
+
// Start units if they were not active previously
+
if !current_active_units.contains_key(unit) {
+
units_to_start.insert(unit.to_string(), ());
+
record_unit(START_LIST_FILE, unit);
+
// We can remove the file now because it has been propagated to the other restart/reload files
+
remove_file_if_exists(RESTART_BY_ACTIVATION_LIST_FILE)
+
.with_context(|| format!("Failed to remove {}", RESTART_BY_ACTIVATION_LIST_FILE))?;
+
for unit in std::fs::read_to_string(RELOAD_BY_ACTIVATION_LIST_FILE)
+
if current_active_units.contains_key(unit)
+
&& !units_to_restart.contains_key(unit)
+
&& !units_to_stop.contains_key(unit)
+
units_to_reload.insert(unit.to_string(), ());
+
record_unit(RELOAD_LIST_FILE, unit);
+
// We can remove the file now because it has been propagated to the other reload file
+
remove_file_if_exists(RELOAD_BY_ACTIVATION_LIST_FILE)
+
.with_context(|| format!("Failed to remove {}", RELOAD_BY_ACTIVATION_LIST_FILE))?;
+
// Restart systemd if necessary. Note that this is done using the current version of systemd,
+
// just in case the new one has trouble communicating with the running pid 1.
+
eprintln!("restarting systemd...");
+
_ = systemd.reexecute(); // we don't get a dbus reply here
+
while !*systemd_reload_status.borrow() {
+
.process(Duration::from_millis(500))
+
.context("Failed to process dbus messages")?;
+
// Forget about previously failed services.
+
.context("Failed to reset failed units")?;
+
// Make systemd reload its units.
+
_ = systemd.reload(); // we don't get a dbus reply here
+
while !*systemd_reload_status.borrow() {
+
.process(Duration::from_millis(500))
+
.context("Failed to process dbus messages")?;
+
.remove_match(reloading_token)
+
.context("Failed to cleanup systemd Reloading match")?;
+
match logind.list_users() {
+
eprintln!("Unable to list users with logind: {err}");
+
for (uid, name, _) in users {
+
eprintln!("reloading user units for {}...", name);
+
let myself = Path::new("/proc/self/exe")
+
.context("Failed to get full path to /proc/self/exe")?;
+
std::process::Command::new(&myself)
+
.env("XDG_RUNTIME_DIR", format!("/run/user/{}", uid))
+
.env("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE", &myself)
+
.map(|mut child| _ = child.wait())
+
.with_context(|| format!("Failed to run user activation for {name}"))?;
+
// Restart sysinit-reactivation.target. This target only exists to restart services ordered
+
// before sysinit.target. We cannot use X-StopOnReconfiguration to restart sysinit.target
+
// because then ALL services of the system would be restarted since all normal services have a
+
// default dependency on sysinit.target. sysinit-reactivation.target ensures that services
+
// ordered BEFORE sysinit.target get re-started in the correct order. Ordering between these
+
// services is respected.
+
eprintln!("restarting {SYSINIT_REACTIVATION_TARGET}");
+
match systemd.restart_unit(SYSINIT_REACTIVATION_TARGET, "replace") {
+
let mut jobs = submitted_jobs.borrow_mut();
+
jobs.insert(job_path, Job::Restart);
+
eprintln!("Failed to restart {SYSINIT_REACTIVATION_TARGET}: {err}");
+
// Wait for the restart job of sysinit-reactivation.service to finish
+
block_on_jobs(&dbus_conn, &submitted_jobs);
+
// Before reloading we need to ensure that the units are still active. They may have been
+
// deactivated because one of their requirements got stopped. If they are inactive but should
+
// have been reloaded, the user probably expects them to be started.
+
if !units_to_reload.is_empty() {
+
for (unit, _) in units_to_reload.clone() {
+
if !unit_is_active(&dbus_conn, &unit)? {
+
// Figure out if we need to start the unit
+
let unit_info = parse_unit(
+
toplevel.join("etc/systemd/system").join(&unit).as_path(),
+
toplevel.join("etc/systemd/system").join(&unit).as_path(),
+
if !parse_systemd_bool(Some(&unit_info), "Unit", "RefuseManualStart", false)
+
|| parse_systemd_bool(Some(&unit_info), "Unit", "X-OnlyManualStart", false)
+
units_to_start.insert(unit.clone(), ());
+
record_unit(START_LIST_FILE, &unit);
+
// Don't reload the unit, reloading would fail
+
units_to_reload.remove(&unit);
+
unrecord_unit(RELOAD_LIST_FILE, &unit);
+
// Reload units that need it. This includes remounting changed mount units.
+
if !units_to_reload.is_empty() {
+
let mut units = units_to_reload
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
eprintln!("reloading the following units: {}", units.join(", "));
+
match systemd.reload_unit(unit, "replace") {
+
.insert(job_path.clone(), Job::Reload);
+
eprintln!("Failed to reload {unit}: {err}");
+
block_on_jobs(&dbus_conn, &submitted_jobs);
+
remove_file_if_exists(RELOAD_LIST_FILE)
+
.with_context(|| format!("Failed to remove {}", RELOAD_LIST_FILE))?;
+
// Restart changed services (those that have to be restarted rather than stopped and started).
+
if !units_to_restart.is_empty() {
+
let mut units = units_to_restart
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
eprintln!("restarting the following units: {}", units.join(", "));
+
match systemd.restart_unit(unit, "replace") {
+
let mut jobs = submitted_jobs.borrow_mut();
+
jobs.insert(job_path, Job::Restart);
+
eprintln!("Failed to restart {unit}: {err}");
+
block_on_jobs(&dbus_conn, &submitted_jobs);
+
remove_file_if_exists(RESTART_LIST_FILE)
+
.with_context(|| format!("Failed to remove {}", RESTART_LIST_FILE))?;
+
// Start all active targets, as well as changed units we stopped above. The latter is necessary
+
// because some may not be dependencies of the targets (i.e., they were manually started).
+
// FIXME: detect units that are symlinks to other units. We shouldn't start both at the same
+
// time because we'll get a "Failed to add path to set" error from systemd.
+
let units_to_start_filtered = filter_units(&units_to_filter, &units_to_start);
+
if !units_to_start_filtered.is_empty() {
+
let mut units = units_to_start_filtered
+
.collect::<Vec<&str>>();
+
units.sort_by_key(|name| name.to_lowercase());
+
eprintln!("starting the following units: {}", units.join(", "));
+
for unit in units_to_start.keys() {
+
match systemd.start_unit(unit, "replace") {
+
let mut jobs = submitted_jobs.borrow_mut();
+
jobs.insert(job_path, Job::Start);
+
eprintln!("Failed to start {unit}: {err}");
+
block_on_jobs(&dbus_conn, &submitted_jobs);
+
remove_file_if_exists(START_LIST_FILE)
+
.with_context(|| format!("Failed to remove {}", START_LIST_FILE))?;
+
for (unit, job, result) in finished_jobs.borrow().values() {
+
match result.as_str() {
+
"timeout" | "failed" | "dependency" => {
+
eprintln!("Failed to {} {}", job, unit);
+
.remove_match(job_removed_token)
+
.context("Failed to cleanup systemd job match")?;
+
// Print failed and new units.
+
let mut failed_units = Vec::new();
+
let mut new_units = Vec::new();
+
// NOTE: We want switch-to-configuration to be able to report to the user any units that failed
+
// to start or units that systemd had to restart due to having previously failed. This is
+
// inherently a race condition between how long our program takes to run and how long the unit
+
// in question takes to potentially fail. The amount of time we wait for new messages on the
+
// bus to settle is purely tuned so that this program is compatible with the Perl
+
// Wait for events from systemd to settle. process() will return true if we have received any
+
// messages on the bus.
+
.process(Duration::from_millis(250))
+
let new_active_units = get_active_units(&systemd)?;
+
for (unit, unit_state) in new_active_units {
+
if &unit_state.state == "failed" {
+
failed_units.push(unit);
+
if unit_state.substate == "auto-restart" && unit.ends_with(".service") {
+
// A unit in auto-restart substate is a failure *if* it previously failed to start
+
let unit_object_path = systemd
+
.context("Failed to get unit info for {unit}")?;
+
let exec_main_status: i32 = dbus_conn
+
"org.freedesktop.systemd1",
+
Duration::from_millis(5000),
+
.get("org.freedesktop.systemd1.Service", "ExecMainStatus")
+
.context("Failed to get ExecMainStatus for {unit}")?;
+
if exec_main_status != 0 {
+
failed_units.push(unit);
+
// Ignore scopes since they are not managed by this script but rather created and managed
+
// by third-party services via the systemd dbus API. This only lists units that are not
+
// failed (including ones that are in auto-restart but have not failed previously)
+
if unit_state.state != "failed"
+
&& !current_active_units.contains_key(&unit)
+
&& !unit.ends_with(".scope")
+
if !new_units.is_empty() {
+
new_units.sort_by_key(|name| name.to_lowercase());
+
"the following new units were started: {}",
+
if !failed_units.is_empty() {
+
failed_units.sort_by_key(|name| name.to_lowercase());
+
"warning: the following units failed: {}",
+
failed_units.join(", ")
+
_ = std::process::Command::new(new_systemd.join("bin/systemctl"))
+
.map(|mut child| child.wait());
+
"finished switching to system configuration {}",
+
"switching to system configuration {} failed (status {})",
+
std::process::exit(exit_code);
+
fn main() -> anyhow::Result<()> {
+
unsafe { nix::libc::geteuid() },
+
std::env::var("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE").ok(),
+
(0, None) => do_system_switch(),
+
(1..=u32::MAX, None) => bail!("This program does not support being ran outside of the switch-to-configuration environment"),
+
(_, Some(parent_exe)) => do_user_switch(parent_exe),
+
use std::collections::HashMap;
+
let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(""));
+
assert!(filesystems.is_empty());
+
assert!(swaps.is_empty());
+
let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
+
assert!(filesystems.is_empty());
+
assert!(swaps.is_empty());
+
let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
+
# This is a generated file. Do not edit!
+
# To make changes, edit the fileSystems and swapDevices NixOS options
+
# in your /etc/nixos/configuration.nix file.
+
# <file system> <mount point> <type> <options> <dump> <pass>
+
/dev/mapper/root / btrfs x-initrd.mount,compress=zstd,noatime,defaults 0 0
+
/dev/disk/by-partlabel/BOOT /boot vfat x-systemd.automount 0 2
+
/dev/disk/by-partlabel/home /home ext4 defaults 0 2
+
/dev/mapper/usr /nix/.ro-store erofs x-initrd.mount,ro 0 2
+
assert_eq!(filesystems.len(), 4);
+
assert_eq!(swaps.len(), 0);
+
let home_fs = filesystems.get("/home").unwrap();
+
assert_eq!(home_fs.fs_type, "ext4");
+
assert_eq!(home_fs.device, "/dev/disk/by-partlabel/home");
+
assert_eq!(home_fs.options, "defaults");
+
super::filter_units(&HashMap::from([]), &HashMap::from([])),
+
&HashMap::from([("foo".to_string(), ())]),
+
&HashMap::from([("foo".to_string(), ()), ("bar".to_string(), ())])
+
HashMap::from([("bar".to_string(), ())])
+
super::compare_units(&HashMap::from([]), &HashMap::from([]))
+
== super::UnitComparison::Equal
+
&HashMap::from([("Unit".to_string(), HashMap::from([]))]),
+
) == super::UnitComparison::Equal
+
"X-Reload-Triggers".to_string(),
+
vec!["foobar".to_string()]
+
) == super::UnitComparison::Equal
+
&HashMap::from([("foobar".to_string(), HashMap::from([]))]),
+
) == super::UnitComparison::UnequalNeedsRestart
+
HashMap::from([("Options".to_string(), vec![])])
+
HashMap::from([("Options".to_string(), vec!["ro".to_string()])])
+
) == super::UnitComparison::UnequalNeedsReload
+
"X-Reload-Triggers".to_string(),
+
vec!["foobar".to_string()]
+
) == super::UnitComparison::UnequalNeedsReload
+
"X-Reload-Triggers".to_string(),
+
vec!["foobar".to_string()]
+
"X-Reload-Triggers".to_string(),
+
vec!["barfoo".to_string()]
+
) == super::UnitComparison::UnequalNeedsReload
+
HashMap::from([("Type".to_string(), vec!["ext4".to_string()])])
+
HashMap::from([("Type".to_string(), vec!["btrfs".to_string()])])
+
) == super::UnitComparison::UnequalNeedsRestart