mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-06-08 06:13:55 +00:00
switch-to-configuration-ng: handle user units migrating to NixOS
When a unit moves from ~/.config/systemd/user (e.g. home-manager) to /etc/systemd/user, the first pass skips it because its FragmentPath still points at the home directory. The per-user activation (nixos-activation.service -> home-manager) then removes the home copy and stops the unit, leaving the now-unmasked NixOS definition inactive until the next login. Record such units up front and run a second pass after nixos-activation.service: daemon-reload, then find units that were shadowed but now aren't anymore, start/restart them as needed, and re-start active targets so any other newly-unmasked dependencies are pulled in. Units that remain shadowed by ~/.config are left alone. The opposite direction (NixOS -> home-manager) already works: the first pass stops the removed /etc unit and home-manager starts the new home copy.
This commit is contained in:
@@ -1246,11 +1246,35 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> {
|
||||
|
||||
let current_active_units = get_active_units(&systemd)?;
|
||||
|
||||
let new_unit_dir = toplevel.join(scope.etc_dir());
|
||||
let fragment_prefix = scope
|
||||
.current_dir()
|
||||
.to_str()
|
||||
.expect("scope dir is valid UTF-8");
|
||||
|
||||
// Units that are currently running from a non-/etc location (typically
|
||||
// ~/.config/systemd/user, i.e. home-manager) but that the new NixOS
|
||||
// configuration also defines. Pass 1 will skip these because of the
|
||||
// FragmentPath filter; if the per-user activation (sd-switch) later drops
|
||||
// its copy, we need a second pass to bring the NixOS-owned definition up.
|
||||
let migration_candidates: Vec<String> = current_active_units
|
||||
.iter()
|
||||
.filter(|(unit, _)| new_unit_dir.join(unit).exists())
|
||||
.filter(|(_, unit_state)| {
|
||||
!unit_state
|
||||
.proxy
|
||||
.get("org.freedesktop.systemd1.Unit", "FragmentPath")
|
||||
.map(|p: String| p.starts_with(fragment_prefix))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|(unit, _)| unit.clone())
|
||||
.collect();
|
||||
|
||||
collect_unit_changes(
|
||||
&toplevel,
|
||||
scope,
|
||||
&old_toplevel.join(scope.etc_dir()),
|
||||
&toplevel.join(scope.etc_dir()),
|
||||
&new_unit_dir,
|
||||
¤t_active_units,
|
||||
&mut units_to_stop,
|
||||
&mut units_to_start,
|
||||
@@ -1359,11 +1383,7 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> {
|
||||
// have been brought up to date. This matches the system → user layering.
|
||||
// Toplevels with system.activatable = false do not ship this unit; mirror
|
||||
// the system scope's tolerance for a missing activate script.
|
||||
if toplevel
|
||||
.join(scope.etc_dir())
|
||||
.join("nixos-activation.service")
|
||||
.exists()
|
||||
{
|
||||
if new_unit_dir.join("nixos-activation.service").exists() {
|
||||
match systemd.restart_unit("nixos-activation.service", "replace") {
|
||||
Ok(_) => {
|
||||
log::debug!("waiting for nixos activation to finish");
|
||||
@@ -1380,6 +1400,80 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: handle units that migrated from another manager to NixOS.
|
||||
// The per-user activation may have removed ~/.config/systemd/user/<unit>
|
||||
// and stopped it (sd-switch); now that the /etc copy is no longer
|
||||
// shadowed, take ownership.
|
||||
if !migration_candidates.is_empty() {
|
||||
// Ensure systemd's view reflects any unit-file removals done by the
|
||||
// per-user activation, in case it did not daemon-reload itself.
|
||||
_ = systemd.reload();
|
||||
|
||||
let active_after = get_active_units(&systemd)?;
|
||||
|
||||
let mut to_restart = HashMap::new();
|
||||
let mut to_start = HashMap::new();
|
||||
|
||||
for unit in &migration_candidates {
|
||||
match active_after.get(unit) {
|
||||
Some(unit_state) => {
|
||||
let now_etc = unit_state
|
||||
.proxy
|
||||
.get("org.freedesktop.systemd1.Unit", "FragmentPath")
|
||||
.map(|p: String| p.starts_with(fragment_prefix))
|
||||
.unwrap_or(false);
|
||||
if now_etc {
|
||||
// Still running with the previous manager's binary;
|
||||
// restart so the /etc definition takes effect.
|
||||
to_restart.insert(unit.clone(), ());
|
||||
}
|
||||
// else: still shadowed by ~/.config, leave it alone.
|
||||
}
|
||||
None => {
|
||||
// Stopped by the previous manager; start the /etc copy.
|
||||
to_start.insert(unit.clone(), ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-start active targets so any other newly-unmasked dependencies are
|
||||
// pulled in as well.
|
||||
for unit in units_to_start.keys() {
|
||||
if unit.ends_with(".target") {
|
||||
to_start.insert(unit.clone(), ());
|
||||
}
|
||||
}
|
||||
|
||||
print_units("restarting (post-activation)", &to_restart);
|
||||
for unit in to_restart.keys() {
|
||||
match systemd.restart_unit(unit, "replace") {
|
||||
Ok(job_path) => {
|
||||
submitted_jobs.borrow_mut().insert(job_path, Job::Restart);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to restart user unit {unit}: {err}");
|
||||
exit_code = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
block_on_jobs(&dbus_conn, &submitted_jobs);
|
||||
|
||||
let to_start_filtered = filter_units(&units_to_filter, &to_start);
|
||||
print_units("starting (post-activation)", &to_start_filtered);
|
||||
for unit in to_start.keys() {
|
||||
match systemd.start_unit(unit, "replace") {
|
||||
Ok(job_path) => {
|
||||
submitted_jobs.borrow_mut().insert(job_path, Job::Start);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to start user unit {unit}: {err}");
|
||||
exit_code = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
block_on_jobs(&dbus_conn, &submitted_jobs);
|
||||
}
|
||||
|
||||
let finished = finished_jobs.borrow();
|
||||
let mut failed_units = Vec::new();
|
||||
for (unit, job, result) in finished.values() {
|
||||
|
||||
Reference in New Issue
Block a user