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