nixfleet_agent/activation/
darwin.rs1use std::process::Stdio;
6
7use super::types::ActivationTarget;
8use anyhow::Result;
9
10use super::{ActivationBackend, ActivationOutcome, RollbackOutcome};
11
12#[derive(Clone, Copy, Debug, Default)]
13pub struct DarwinBackend;
14
15impl ActivationBackend for DarwinBackend {
16 async fn is_switch_in_progress(&self) -> bool {
17 false
18 }
19 async fn read_unit_exit_code(&self, _unit_name: &str) -> Option<i32> {
20 None
21 }
22 async fn fire_switch(
23 &self,
24 target: &ActivationTarget,
25 store_path: &str,
26 ) -> Result<Option<ActivationOutcome>> {
27 fire_switch(target, store_path).await
28 }
29 async fn fire_rollback(&self, target_basename: &str) -> Result<Option<RollbackOutcome>> {
30 fire_rollback(target_basename).await
31 }
32}
33
34async fn fire_switch(
35 target: &ActivationTarget,
36 store_path: &str,
37) -> Result<Option<ActivationOutcome>> {
38 use std::os::unix::process::CommandExt;
39
40 tracing::info!(
41 target_closure = %target.closure_hash,
42 "agent: firing darwin activation (setsid-detached activate-user + activate)",
43 );
44
45 let activate_user = format!("{store_path}/activate-user");
47 if std::path::Path::new(&activate_user).exists() {
48 let mut cmd = std::process::Command::new(&activate_user);
49 cmd.stdin(Stdio::null());
50 attach_activate_log_to(&mut cmd, ACTIVATE_LOG);
51 unsafe {
53 cmd.pre_exec(|| {
54 if libc::setsid() == -1 {
55 return Err(std::io::Error::last_os_error());
56 }
57 Ok(())
58 });
59 }
60 match cmd.spawn() {
61 Ok(_child) => {
62 tracing::debug!(
63 target_closure = %target.closure_hash,
64 "agent: darwin activate-user fired (detached)",
65 );
66 }
67 Err(err) => {
68 tracing::warn!(
69 target_closure = %target.closure_hash,
70 error = %err,
71 "agent: darwin activate-user spawn failed (non-fatal); continuing to system activate",
72 );
73 }
74 }
75 } else {
76 tracing::debug!(
77 target_closure = %target.closure_hash,
78 "agent: darwin activate-user absent; skipping (modern closure shape)",
79 );
80 }
81
82 let activate = format!("{store_path}/activate");
84 let mut cmd = std::process::Command::new(&activate);
85 cmd.stdin(Stdio::null());
86 attach_activate_log_to(&mut cmd, ACTIVATE_LOG);
87 unsafe {
88 cmd.pre_exec(|| {
89 if libc::setsid() == -1 {
90 return Err(std::io::Error::last_os_error());
91 }
92 Ok(())
93 });
94 }
95 match cmd.spawn() {
96 Ok(_child) => {
97 tracing::info!(
98 target_closure = %target.closure_hash,
99 "agent: darwin activate fired (setsid-detached); polling current-system",
100 );
101 Ok(None)
102 }
103 Err(err) => {
104 tracing::error!(
105 target_closure = %target.closure_hash,
106 error = %err,
107 "agent: darwin activate spawn failed",
108 );
109 Ok(Some(ActivationOutcome::SwitchFailed {
110 phase: "darwin-activate-spawn".to_string(),
111 exit_code: None,
112 }))
113 }
114 }
115}
116
117async fn fire_rollback(target_basename: &str) -> Result<Option<RollbackOutcome>> {
118 use std::os::unix::process::CommandExt;
119
120 let store_path = format!("/nix/store/{target_basename}");
121 let activate = format!("{store_path}/activate");
122 if !std::path::Path::new(&activate).exists() {
123 tracing::error!(
124 activate = %activate,
125 "agent: darwin rollback target has no activate script",
126 );
127 return Ok(Some(RollbackOutcome::Failed {
128 phase: "darwin-activate-missing".to_string(),
129 exit_code: None,
130 }));
131 }
132
133 tracing::info!(
134 target = %target_basename,
135 "agent: firing darwin rollback (setsid-detached activate)",
136 );
137 let mut cmd = std::process::Command::new(&activate);
138 cmd.stdin(Stdio::null());
139 attach_activate_log_to(&mut cmd, ACTIVATE_LOG);
140 unsafe {
141 cmd.pre_exec(|| {
142 if libc::setsid() == -1 {
143 return Err(std::io::Error::last_os_error());
144 }
145 Ok(())
146 });
147 }
148 match cmd.spawn() {
149 Ok(_child) => Ok(None),
150 Err(err) => {
151 tracing::error!(
152 target = %target_basename,
153 error = %err,
154 "agent: darwin rollback activate spawn failed",
155 );
156 Ok(Some(RollbackOutcome::Failed {
157 phase: "darwin-activate-spawn".to_string(),
158 exit_code: None,
159 }))
160 }
161 }
162}
163
164const ACTIVATE_LOG: &str = "/var/log/nixfleet-activate.log";
165
166fn attach_activate_log_to(cmd: &mut std::process::Command, path: &str) {
168 match std::fs::OpenOptions::new()
169 .create(true)
170 .append(true)
171 .open(path)
172 {
173 Ok(out) => {
174 let err = match out.try_clone() {
175 Ok(c) => c,
176 Err(e) => {
177 tracing::warn!(
178 path = path,
179 error = %e,
180 "could not clone activate log handle; using inherit",
181 );
182 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
183 return;
184 }
185 };
186 cmd.stdout(out).stderr(err);
187 }
188 Err(e) => {
189 tracing::warn!(
190 path = path,
191 error = %e,
192 "could not open activate log; using inherit",
193 );
194 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[tokio::test]
204 async fn darwin_backend_is_switch_in_progress_returns_false() {
205 assert!(!DarwinBackend.is_switch_in_progress().await);
206 }
207
208 #[tokio::test]
209 async fn darwin_backend_read_unit_exit_code_returns_none() {
210 assert_eq!(DarwinBackend.read_unit_exit_code("anything").await, None);
211 }
212
213 #[test]
214 fn attach_activate_log_falls_back_to_inherit_when_path_unwritable() {
215 let dir = tempfile::tempdir().expect("tempdir");
216 let unwritable = dir.path().join("does-not-exist").join("nope.log");
217 let mut cmd = std::process::Command::new("true");
218 attach_activate_log_to(&mut cmd, unwritable.to_str().unwrap());
219 }
220
221 #[test]
222 fn attach_activate_log_succeeds_when_path_writable() {
223 let dir = tempfile::tempdir().expect("tempdir");
224 let path = dir.path().join("activate.log");
225 let mut cmd = std::process::Command::new("true");
226 attach_activate_log_to(&mut cmd, path.to_str().unwrap());
227 assert!(path.exists(), "log file should be created");
228 }
229
230 #[test]
231 fn darwin_backend_default_is_unit_struct() {
232 let _b: DarwinBackend = DarwinBackend;
233 #[allow(clippy::default_constructed_unit_structs)]
234 let _: DarwinBackend = DarwinBackend::default();
235 }
236}