lean_ctx/
proxy_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.proxy";
6#[cfg(target_os = "linux")]
7const SYSTEMD_SERVICE: &str = "lean-ctx-proxy";
8
9pub fn install(port: u16, quiet: bool) {
10 let binary = find_binary();
11 if binary.is_empty() {
12 if !quiet {
13 tracing::error!("Cannot find lean-ctx binary for autostart");
14 }
15 return;
16 }
17
18 #[cfg(target_os = "macos")]
19 install_launchagent(&binary, port, quiet);
20
21 #[cfg(target_os = "linux")]
22 install_systemd(&binary, port, 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 proxy start --port={port}");
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
74pub fn is_supported() -> bool {
78 cfg!(any(target_os = "macos", target_os = "linux"))
79}
80
81pub fn is_installed() -> bool {
83 #[cfg(target_os = "macos")]
84 {
85 launchagent_path().exists()
86 }
87 #[cfg(target_os = "linux")]
88 {
89 systemd_path().exists()
90 }
91 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
92 {
93 false
94 }
95}
96
97pub fn is_loaded() -> bool {
100 #[cfg(target_os = "macos")]
101 {
102 is_installed() && crate::core::launchd::is_loaded(PLIST_LABEL)
103 }
104 #[cfg(target_os = "linux")]
105 {
106 is_installed()
107 && std::process::Command::new("systemctl")
108 .args(["--user", "is-active", "--quiet", SYSTEMD_SERVICE])
109 .status()
110 .is_ok_and(|status| status.success())
111 }
112 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
113 {
114 false
115 }
116}
117
118pub fn status() {
119 #[cfg(target_os = "macos")]
120 {
121 let plist_path = launchagent_path();
122 if plist_path.exists() {
123 println!(" LaunchAgent: installed at {}", plist_path.display());
124 if crate::core::launchd::is_loaded(PLIST_LABEL) {
125 println!(" Status: loaded");
126 } else {
127 println!(" Status: not loaded (run: lean-ctx proxy start)");
128 }
129 } else {
130 println!(" LaunchAgent: not installed");
131 }
132 }
133
134 #[cfg(target_os = "linux")]
135 {
136 let service_path = systemd_path();
137 if service_path.exists() {
138 println!(" systemd user service: installed");
139 let output = std::process::Command::new("systemctl")
140 .args(["--user", "is-active", SYSTEMD_SERVICE])
141 .output();
142 match output {
143 Ok(o) => {
144 let state = String::from_utf8_lossy(&o.stdout).trim().to_string();
145 println!(" Status: {state}");
146 }
147 Err(_) => println!(" Status: unknown"),
148 }
149 } else {
150 println!(" systemd service: not installed");
151 }
152 }
153
154 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
155 {
156 println!(" Autostart not available on this platform");
157 }
158}
159
160#[cfg(target_os = "macos")]
161fn launchagent_path() -> PathBuf {
162 dirs::home_dir()
163 .unwrap_or_else(|| PathBuf::from("/tmp"))
164 .join("Library/LaunchAgents")
165 .join(format!("{PLIST_LABEL}.plist"))
166}
167
168#[cfg(target_os = "macos")]
169fn install_launchagent(binary: &str, port: u16, quiet: bool) {
170 let plist_dir = dirs::home_dir()
171 .unwrap_or_else(|| PathBuf::from("/tmp"))
172 .join("Library/LaunchAgents");
173 let _ = std::fs::create_dir_all(&plist_dir);
174
175 let plist_path = plist_dir.join(format!("{PLIST_LABEL}.plist"));
176 let log_dir = crate::core::paths::state_dir()
180 .unwrap_or_else(|_| std::env::temp_dir().join("lean-ctx"))
181 .join("logs");
182 let _ = std::fs::create_dir_all(&log_dir);
183
184 let port_arg = format!("--port={port}");
187 let program_args = crate::core::tcc_guard_sandbox::program_args_xml(
188 &crate::core::tcc_guard_sandbox::wrap_launchd_args(binary, &["proxy", "start", &port_arg]),
189 " ",
190 );
191
192 let env_vars = crate::core::tcc_guard_sandbox::pinned_layout_env_xml();
199
200 let plist = format!(
201 r#"<?xml version="1.0" encoding="UTF-8"?>
202<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
203<plist version="1.0">
204<dict>
205 <key>Label</key>
206 <string>{PLIST_LABEL}</string>
207 <key>ProgramArguments</key>
208 <array>
209{program_args}
210 </array>
211{env_vars} <key>RunAtLoad</key>
212 <true/>
213 <key>KeepAlive</key>
214 <true/>
215 <key>StandardOutPath</key>
216 <string>{stdout}</string>
217 <key>StandardErrorPath</key>
218 <string>{stderr}</string>
219</dict>
220</plist>"#,
221 stdout = log_dir.join("proxy.stdout.log").display(),
222 stderr = log_dir.join("proxy.stderr.log").display(),
223 );
224
225 let _ = std::fs::write(&plist_path, &plist);
226
227 let ok = crate::core::launchd::bootstrap(PLIST_LABEL, &plist_path);
228
229 if !quiet {
230 if ok {
231 println!(" Installed LaunchAgent: {}", plist_path.display());
232 println!(" Proxy will start on login and restart if stopped");
233 } else {
234 println!(" Created LaunchAgent at {}", plist_path.display());
235 println!(" Load reported a problem; check: launchctl print {PLIST_LABEL}");
236 }
237 }
238}
239
240#[cfg(target_os = "macos")]
241fn uninstall_launchagent(quiet: bool) {
242 let plist_path = launchagent_path();
243 if !plist_path.exists() {
244 if !quiet {
245 println!(" LaunchAgent not installed, nothing to remove");
246 }
247 return;
248 }
249
250 crate::core::launchd::bootout(PLIST_LABEL, &plist_path);
251
252 let _ = std::fs::remove_file(&plist_path);
253 if !quiet {
254 println!(" Removed LaunchAgent: {}", plist_path.display());
255 }
256}
257
258#[cfg(target_os = "linux")]
259fn systemd_path() -> PathBuf {
260 dirs::home_dir()
261 .unwrap_or_else(|| PathBuf::from("/tmp"))
262 .join(".config/systemd/user")
263 .join(format!("{SYSTEMD_SERVICE}.service"))
264}
265
266#[cfg(target_os = "linux")]
267fn install_systemd(binary: &str, port: u16, quiet: bool) {
268 let service_dir = dirs::home_dir()
269 .unwrap_or_else(|| PathBuf::from("/tmp"))
270 .join(".config/systemd/user");
271 let _ = std::fs::create_dir_all(&service_dir);
272
273 let service_path = service_dir.join(format!("{SYSTEMD_SERVICE}.service"));
274
275 let unit = format!(
276 r"[Unit]
277Description=lean-ctx API Proxy
278After=network.target
279StartLimitIntervalSec=300
280StartLimitBurst=5
281
282[Service]
283Type=simple
284ExecStart={binary} proxy start --port={port}
285Restart=on-failure
286RestartSec=5
287StandardOutput=journal
288StandardError=journal
289Environment=RUST_LOG=info
290
291[Install]
292WantedBy=default.target
293"
294 );
295
296 let _ = std::fs::write(&service_path, &unit);
297
298 let _ = std::process::Command::new("systemctl")
299 .args(["--user", "daemon-reload"])
300 .output();
301
302 let result = std::process::Command::new("systemctl")
303 .args(["--user", "enable", "--now", SYSTEMD_SERVICE])
304 .output();
305
306 if !quiet {
307 match result {
308 Ok(o) if o.status.success() => {
309 println!(" Installed systemd user service: {SYSTEMD_SERVICE}");
310 println!(" Proxy will start on login and restart if stopped");
311 }
312 Ok(o) => {
313 let err = String::from_utf8_lossy(&o.stderr);
314 println!(" Created service file but enable failed: {err}");
315 }
316 Err(e) => {
317 println!(" Created service file at {}", service_path.display());
318 println!(" Could not enable: {e}");
319 }
320 }
321 }
322}
323
324#[cfg(target_os = "linux")]
325fn uninstall_systemd(quiet: bool) {
326 let service_path = systemd_path();
327 if !service_path.exists() {
328 if !quiet {
329 println!(" systemd service not installed, nothing to remove");
330 }
331 return;
332 }
333
334 let _ = std::process::Command::new("systemctl")
335 .args(["--user", "stop", SYSTEMD_SERVICE])
336 .output();
337 let _ = std::process::Command::new("systemctl")
338 .args(["--user", "disable", SYSTEMD_SERVICE])
339 .output();
340 let _ = std::fs::remove_file(&service_path);
341 let _ = std::process::Command::new("systemctl")
342 .args(["--user", "daemon-reload"])
343 .output();
344
345 if !quiet {
346 println!(" Removed systemd service: {SYSTEMD_SERVICE}");
347 }
348}
349
350pub fn find_binary() -> String {
351 crate::core::portable_binary::resolve_portable_binary()
352}