Skip to main content

lean_ctx/
daemon_autostart.rs

1#[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// ---------------------------------------------------------------------------
99// macOS LaunchAgent
100// ---------------------------------------------------------------------------
101
102#[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// ---------------------------------------------------------------------------
189// Linux systemd
190// ---------------------------------------------------------------------------
191
192#[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    // Hint about linger for headless/server use (needed for boot-time start without login)
259    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}