nixfleet_agent/activation/
profile.rs

1//! Profile-flip helpers: self-correction after a concurrent profile-mutator,
2//! and resolution of the rolled-back target's basename.
3
4use anyhow::{Context, Result, anyhow};
5use tokio::process::Command;
6
7/// `Err` only when self-correction itself failed; caller treats as non-fatal.
8pub(super) async fn self_correct_profile(expected_store_path: &str) -> Result<()> {
9    let profile = "/nix/var/nix/profiles/system";
10    if profile_matches(expected_store_path, profile) {
11        return Ok(());
12    }
13
14    tracing::warn!(
15        expected = %expected_store_path,
16        profile = profile,
17        "agent: profile mismatch after fire-and-forget - re-running nix-env --set",
18    );
19    let status = Command::new("nix-env")
20        .arg("--profile")
21        .arg(profile)
22        .arg("--set")
23        .arg(expected_store_path)
24        .status()
25        .await
26        .with_context(|| "spawn nix-env --set (self-correction)")?;
27    if !status.success() {
28        return Err(anyhow!(
29            "nix-env --set self-correction exited {:?}",
30            status.code()
31        ));
32    }
33    if !profile_matches(expected_store_path, profile) {
34        return Err(anyhow!(
35            "profile still mismatched after nix-env --set self-correction",
36        ));
37    }
38    tracing::info!("agent: profile self-corrected successfully");
39    Ok(())
40}
41
42// GOTCHA: profile is two-level symlink: profile -> `system-<N>-link` -> `/nix/store/<basename>`.
43fn profile_matches(expected_store_path: &str, profile_path: &str) -> bool {
44    let Ok(gen_link) = std::fs::read_link(profile_path) else {
45        return false;
46    };
47    let abs_gen_link = if gen_link.is_relative() {
48        let parent = std::path::Path::new(profile_path)
49            .parent()
50            .unwrap_or(std::path::Path::new("/"));
51        parent.join(&gen_link)
52    } else {
53        gen_link
54    };
55    let final_target = match std::fs::read_link(&abs_gen_link) {
56        Ok(t) => t,
57        Err(_) => abs_gen_link,
58    };
59    final_target.to_string_lossy() == expected_store_path
60}
61
62pub(super) fn resolve_profile_target() -> Result<String> {
63    let profile = std::path::Path::new("/nix/var/nix/profiles/system");
64    let gen_link =
65        std::fs::read_link(profile).with_context(|| "readlink /nix/var/nix/profiles/system")?;
66    let abs_gen_link = if gen_link.is_relative() {
67        profile
68            .parent()
69            .unwrap_or(std::path::Path::new("/"))
70            .join(&gen_link)
71    } else {
72        gen_link.clone()
73    };
74    let store_path = std::fs::read_link(&abs_gen_link)
75        .with_context(|| format!("readlink {}", abs_gen_link.display()))?;
76    let basename = store_path
77        .file_name()
78        .and_then(|n| n.to_str())
79        .ok_or_else(|| anyhow!("non-utf8 basename: {}", store_path.display()))?
80        .to_string();
81    Ok(basename)
82}