oxicode/foundation/
brain_control.rs1use std::path::PathBuf;
18use std::process::Stdio;
19
20pub const BRAIN_SERVICE_LABEL: &str = "com.oxi.oxibrain";
22
23pub fn brain_plist_path() -> Option<PathBuf> {
25 let home = std::env::var_os("HOME")?;
26 let mut p = PathBuf::from(home);
27 p.push("Library");
28 p.push("LaunchAgents");
29 p.push(format!("{BRAIN_SERVICE_LABEL}.plist"));
30 Some(p)
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum ReviveAction {
36 BootstrapPlist(PathBuf),
38 KickstartService,
40 SpawnDetached(PathBuf),
42 InstallNeeded,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct BrainControlReport {
49 pub binary: Option<PathBuf>,
51 pub plist: Option<PathBuf>,
53 pub service_loaded: bool,
55}
56
57pub fn revive_plan(report: &BrainControlReport) -> ReviveAction {
59 let Some(binary) = report.binary.clone() else {
60 return ReviveAction::InstallNeeded;
61 };
62 match &report.plist {
63 Some(plist) if !report.service_loaded => ReviveAction::BootstrapPlist(plist.clone()),
64 Some(_) => ReviveAction::KickstartService,
65 None => ReviveAction::SpawnDetached(binary),
66 }
67}
68
69pub fn find_oxibrain_binary() -> Option<PathBuf> {
72 if let Some(home) = std::env::var_os("HOME") {
73 let mut p = PathBuf::from(home);
74 p.push(".cargo");
75 p.push("bin");
76 p.push("oxibrain");
77 if p.is_file() {
78 return Some(p);
79 }
80 }
81 let path = std::env::var_os("PATH")?;
82 for dir in std::env::split_paths(&path) {
83 let candidate = dir.join("oxibrain");
84 if candidate.is_file() {
85 return Some(candidate);
86 }
87 }
88 None
89}
90
91pub fn service_loaded() -> bool {
93 if !cfg!(target_os = "macos") {
94 return false;
95 }
96 let uid = std::process::id();
97 let _ = uid;
98 let user = whoami_uid();
99 let out = std::process::Command::new("launchctl")
100 .args(["print", &format!("gui/{user}/{BRAIN_SERVICE_LABEL}")])
101 .stdout(Stdio::null())
102 .stderr(Stdio::null())
103 .status();
104 matches!(out, Ok(st) if st.success())
105}
106
107pub fn should_auto_revive(
113 memory_enabled: bool,
114 daemon_down: bool,
115 already_attempted: bool,
116) -> bool {
117 memory_enabled && daemon_down && !already_attempted
118}
119fn whoami_uid() -> String {
120 std::process::Command::new("id")
121 .arg("-u")
122 .output()
123 .ok()
124 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
125 .unwrap_or_else(|| "501".to_string())
126}
127
128pub fn probe_control() -> BrainControlReport {
130 BrainControlReport {
131 binary: find_oxibrain_binary(),
132 plist: brain_plist_path().filter(|p| p.is_file()),
133 service_loaded: service_loaded(),
134 }
135}
136
137pub async fn revive() -> Result<String, String> {
140 let report = probe_control();
141 let plan = revive_plan(&report);
142 match plan {
143 ReviveAction::InstallNeeded => Err(
144 "oxibrain binary not found — install it first (e.g. `cargo install oxibrain` \
145 from the oxibrain repo), then run /brain again"
146 .to_string(),
147 ),
148 ReviveAction::BootstrapPlist(plist) => {
149 let user = whoami_uid();
150 let domain = format!("gui/{user}");
151 let status = std::process::Command::new("launchctl")
152 .args(["bootstrap", &domain])
153 .arg(&plist)
154 .status()
155 .map_err(|e| format!("launchctl bootstrap failed: {e}"))?;
156 if !status.success() {
157 return Err(format!(
158 "launchctl bootstrap exited {} — check `launchctl print {domain}/{BRAIN_SERVICE_LABEL}`",
159 status.code().unwrap_or(-1)
160 ));
161 }
162 wait_for_ping("bootstrapped the launchd service").await
163 }
164 ReviveAction::KickstartService => {
165 let user = whoami_uid();
166 let target = format!("gui/{user}/{BRAIN_SERVICE_LABEL}");
167 let status = std::process::Command::new("launchctl")
168 .args(["kickstart", &target])
169 .status()
170 .map_err(|e| format!("launchctl kickstart failed: {e}"))?;
171 if !status.success() {
172 return Err(format!(
173 "launchctl kickstart exited {} — check `launchctl print {target}`",
174 status.code().unwrap_or(-1)
175 ));
176 }
177 wait_for_ping("kicked the launchd service").await
178 }
179 ReviveAction::SpawnDetached(binary) => {
180 let socket = crate::foundation::brain::default_socket_path();
181 let mut cmd = std::process::Command::new(&binary);
182 cmd.args(["serve", "--daemon"])
183 .arg("--socket")
184 .arg(&socket)
185 .stdin(Stdio::null())
186 .stdout(Stdio::null())
187 .stderr(Stdio::null());
188 #[cfg(unix)]
189 {
190 use std::os::unix::process::CommandExt;
191 cmd.process_group(0);
192 }
193 cmd.spawn()
194 .map_err(|e| format!("spawning oxibrain failed: {e}"))?;
195 wait_for_ping("spawned a detached daemon").await
196 }
197 }
198}
199
200async fn wait_for_ping(action: &str) -> Result<String, String> {
202 let backend = crate::foundation::brain::BrainMemoryBackend::new(
203 crate::foundation::brain::default_socket_path(),
204 );
205 for _ in 0..20 {
206 if backend.ping().await.is_ok() {
207 return Ok(format!("{action} — daemon is answering pings"));
208 }
209 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
210 }
211 Err(format!(
212 "{action}, but the daemon did not answer within 5s — check the log at \
213 ~/.oxi/brain/daemon.log"
214 ))
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn bin() -> PathBuf {
222 PathBuf::from("/usr/local/bin/oxibrain")
223 }
224
225 #[test]
226 fn plan_without_binary_requires_install() {
227 let report = BrainControlReport {
228 binary: None,
229 plist: Some(PathBuf::from("/Library/LaunchAgents/x.plist")),
230 service_loaded: false,
231 };
232 assert_eq!(revive_plan(&report), ReviveAction::InstallNeeded);
233 }
234
235 #[test]
236 fn plan_bootstraps_an_unloaded_service() {
237 let report = BrainControlReport {
238 binary: Some(bin()),
239 plist: Some(PathBuf::from("/Library/LaunchAgents/x.plist")),
240 service_loaded: false,
241 };
242 assert_eq!(
243 revive_plan(&report),
244 ReviveAction::BootstrapPlist(PathBuf::from("/Library/LaunchAgents/x.plist"))
245 );
246 }
247
248 #[test]
249 fn plan_kickstarts_a_loaded_service() {
250 let report = BrainControlReport {
251 binary: Some(bin()),
252 plist: Some(PathBuf::from("/Library/LaunchAgents/x.plist")),
253 service_loaded: true,
254 };
255 assert_eq!(revive_plan(&report), ReviveAction::KickstartService);
256 }
257
258 #[test]
259 fn plan_spawns_detached_without_launchd() {
260 let report = BrainControlReport {
261 binary: Some(bin()),
262 plist: None,
263 service_loaded: false,
264 };
265 assert_eq!(revive_plan(&report), ReviveAction::SpawnDetached(bin()));
266 }
267
268 #[test]
269 fn auto_revive_gates() {
270 assert!(should_auto_revive(true, true, false), "enabled+down → go");
271 assert!(
272 !should_auto_revive(false, true, false),
273 "memory disabled → never"
274 );
275 assert!(
276 !should_auto_revive(true, true, true),
277 "one attempt per session — no retry loops"
278 );
279 assert!(
280 !should_auto_revive(true, false, false),
281 "healthy daemon → nothing to do"
282 );
283 }
284}