lean_ctx/
daemon_autostart.rs1#[cfg(any(target_os = "macos", target_os = "linux"))]
2use std::path::PathBuf;
3
4#[cfg(target_os = "macos")]
5const PLIST_LABEL: &str = "com.leanctx.daemon";
6#[cfg(target_os = "linux")]
7const SYSTEMD_SERVICE: &str = "lean-ctx-daemon";
8
9pub fn install(quiet: bool) {
10 let binary = crate::proxy_autostart::find_binary();
11 if binary.is_empty() {
12 if !quiet {
13 tracing::error!("Cannot find lean-ctx binary for daemon autostart");
14 }
15 return;
16 }
17
18 #[cfg(target_os = "macos")]
19 install_launchagent(&binary, quiet);
20
21 #[cfg(target_os = "linux")]
22 install_systemd(&binary, quiet);
23
24 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
25 {
26 let _ = (&binary, quiet);
27 println!(" Autostart not supported on this platform");
28 println!(" Run manually: lean-ctx serve -d");
29 }
30}
31
32pub fn stop() {
33 #[cfg(target_os = "macos")]
34 {
35 let plist_path = launchagent_path();
36 if plist_path.exists() {
37 let _ = std::process::Command::new("launchctl")
38 .args(["unload", &plist_path.to_string_lossy()])
39 .output();
40 }
41 }
42
43 #[cfg(target_os = "linux")]
44 {
45 let _ = std::process::Command::new("systemctl")
46 .args(["--user", "stop", SYSTEMD_SERVICE])
47 .output();
48 }
49}
50
51pub fn start() {
52 #[cfg(target_os = "macos")]
53 {
54 let plist_path = launchagent_path();
55 if plist_path.exists() {
56 let _ = std::process::Command::new("launchctl")
57 .args(["load", &plist_path.to_string_lossy()])
58 .output();
59 }
60 }
61
62 #[cfg(target_os = "linux")]
63 {
64 let _ = std::process::Command::new("systemctl")
65 .args(["--user", "start", SYSTEMD_SERVICE])
66 .output();
67 }
68}
69
70pub fn uninstall(quiet: bool) {
71 #[cfg(target_os = "macos")]
72 uninstall_launchagent(quiet);
73
74 #[cfg(target_os = "linux")]
75 uninstall_systemd(quiet);
76
77 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
78 let _ = quiet;
79}
80
81pub fn is_installed() -> bool {
82 #[cfg(target_os = "macos")]
83 {
84 launchagent_path().exists()
85 }
86 #[cfg(target_os = "linux")]
87 {
88 if !systemd_path().exists() {
89 return false;
90 }
91 std::process::Command::new("systemctl")
92 .args(["--user", "is-enabled", "--quiet", SYSTEMD_SERVICE])
93 .status()
94 .is_ok_and(|s| s.success())
95 }
96 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
97 {
98 false
99 }
100}
101
102#[cfg(target_os = "macos")]
107fn launchagent_path() -> PathBuf {
108 dirs::home_dir()
109 .unwrap_or_else(|| PathBuf::from("/tmp"))
110 .join("Library/LaunchAgents")
111 .join(format!("{PLIST_LABEL}.plist"))
112}
113
114#[cfg(target_os = "macos")]
115fn install_launchagent(binary: &str, quiet: bool) {
116 let la_dir = dirs::home_dir()
117 .unwrap_or_else(|| PathBuf::from("/tmp"))
118 .join("Library/LaunchAgents");
119 let _ = std::fs::create_dir_all(&la_dir);
120
121 let plist_path = la_dir.join(format!("{PLIST_LABEL}.plist"));
122 let data_dir = dirs::data_local_dir()
123 .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".local/share"))
124 .join("lean-ctx");
125
126 let _ = std::fs::create_dir_all(&data_dir);
127
128 let plist = format!(
129 r#"<?xml version="1.0" encoding="UTF-8"?>
130<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
131<plist version="1.0">
132<dict>
133 <key>Label</key>
134 <string>{PLIST_LABEL}</string>
135 <key>ProgramArguments</key>
136 <array>
137 <string>{binary}</string>
138 <string>serve</string>
139 <string>--_foreground-daemon</string>
140 </array>
141 <key>RunAtLoad</key>
142 <true/>
143 <key>KeepAlive</key>
144 <dict>
145 <key>SuccessfulExit</key>
146 <false/>
147 </dict>
148 <key>ThrottleInterval</key>
149 <integer>10</integer>
150 <key>StandardOutPath</key>
151 <string>{stdout}</string>
152 <key>StandardErrorPath</key>
153 <string>{stderr}</string>
154</dict>
155</plist>
156"#,
157 stdout = data_dir.join("daemon-stdout.log").display(),
158 stderr = data_dir.join("daemon-stderr.log").display(),
159 );
160
161 let _ = std::fs::write(&plist_path, &plist);
162
163 let _ = std::process::Command::new("launchctl")
164 .args(["unload", &plist_path.to_string_lossy()])
165 .output();
166 let result = std::process::Command::new("launchctl")
167 .args(["load", "-w", &plist_path.to_string_lossy()])
168 .output();
169
170 if !quiet {
171 match result {
172 Ok(o) if o.status.success() => {
173 println!(" Installed LaunchAgent: {PLIST_LABEL}");
174 println!(" Daemon will start on login and restart if stopped");
175 }
176 Ok(o) => {
177 let err = String::from_utf8_lossy(&o.stderr);
178 println!(" Created plist but load failed: {err}");
179 }
180 Err(e) => {
181 println!(" Created plist at {}", plist_path.display());
182 println!(" Could not load: {e}");
183 }
184 }
185 }
186}
187
188#[cfg(target_os = "macos")]
189fn uninstall_launchagent(quiet: bool) {
190 let plist_path = launchagent_path();
191 if !plist_path.exists() {
192 if !quiet {
193 println!(" Daemon LaunchAgent not installed, nothing to remove");
194 }
195 return;
196 }
197 let _ = std::process::Command::new("launchctl")
198 .args(["unload", &plist_path.to_string_lossy()])
199 .output();
200 let _ = std::fs::remove_file(&plist_path);
201 if !quiet {
202 println!(" Removed daemon LaunchAgent");
203 }
204}
205
206#[cfg(target_os = "linux")]
211fn systemd_path() -> PathBuf {
212 dirs::home_dir()
213 .unwrap_or_else(|| PathBuf::from("/tmp"))
214 .join(".config/systemd/user")
215 .join(format!("{SYSTEMD_SERVICE}.service"))
216}
217
218#[cfg(target_os = "linux")]
219fn install_systemd(binary: &str, quiet: bool) {
220 let service_dir = dirs::home_dir()
221 .unwrap_or_else(|| PathBuf::from("/tmp"))
222 .join(".config/systemd/user");
223 let _ = std::fs::create_dir_all(&service_dir);
224
225 let service_path = service_dir.join(format!("{SYSTEMD_SERVICE}.service"));
226
227 let unit = format!(
228 r"[Unit]
229Description=lean-ctx IPC Daemon
230After=network.target
231StartLimitIntervalSec=300
232StartLimitBurst=5
233
234[Service]
235Type=simple
236ExecStart={binary} serve --_foreground-daemon
237Restart=on-failure
238RestartSec=5
239StandardOutput=journal
240StandardError=journal
241Environment=RUST_LOG=info
242
243[Install]
244WantedBy=default.target
245"
246 );
247
248 let _ = std::fs::write(&service_path, &unit);
249
250 let _ = std::process::Command::new("systemctl")
251 .args(["--user", "daemon-reload"])
252 .output();
253
254 let result = std::process::Command::new("systemctl")
255 .args(["--user", "enable", "--now", SYSTEMD_SERVICE])
256 .output();
257
258 match result {
259 Ok(o) if o.status.success() => {
260 if !quiet {
261 println!(" Installed systemd user service: {SYSTEMD_SERVICE}");
262 println!(" Daemon will start on login and restart if stopped");
263 }
264 }
265 Ok(o) => {
266 let err = String::from_utf8_lossy(&o.stderr);
267 eprintln!(" Created service file but `systemctl enable` failed: {err}");
268 eprintln!(" Try manually: systemctl --user enable --now {SYSTEMD_SERVICE}");
269 }
270 Err(e) => {
271 eprintln!(" Created service file at {}", service_path.display());
272 eprintln!(" Could not run systemctl: {e}");
273 }
274 }
275
276 if !quiet {
278 if let Ok(o) = std::process::Command::new("loginctl")
279 .args(["show-user", &whoami(), "-p", "Linger", "--value"])
280 .output()
281 {
282 let val = String::from_utf8_lossy(&o.stdout).trim().to_string();
283 if val != "yes" {
284 println!(
285 " Note: for daemon to start at boot (without login), run:\n \
286 loginctl enable-linger {}",
287 whoami()
288 );
289 }
290 }
291 }
292}
293
294#[cfg(target_os = "linux")]
295fn whoami() -> String {
296 std::env::var("USER")
297 .or_else(|_| std::env::var("LOGNAME"))
298 .unwrap_or_else(|_| "$(whoami)".to_string())
299}
300
301#[cfg(target_os = "linux")]
302fn uninstall_systemd(quiet: bool) {
303 let service_path = systemd_path();
304 if !service_path.exists() {
305 if !quiet {
306 println!(" Daemon systemd service not installed, nothing to remove");
307 }
308 return;
309 }
310
311 let _ = std::process::Command::new("systemctl")
312 .args(["--user", "stop", SYSTEMD_SERVICE])
313 .output();
314 let _ = std::process::Command::new("systemctl")
315 .args(["--user", "disable", SYSTEMD_SERVICE])
316 .output();
317 let _ = std::fs::remove_file(&service_path);
318 let _ = std::process::Command::new("systemctl")
319 .args(["--user", "daemon-reload"])
320 .output();
321
322 if !quiet {
323 println!(" Removed daemon systemd service: {SYSTEMD_SERVICE}");
324 }
325}