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