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 plist = format!(
172 r#"<?xml version="1.0" encoding="UTF-8"?>
173<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
174<plist version="1.0">
175<dict>
176 <key>Label</key>
177 <string>{PLIST_LABEL}</string>
178 <key>ProgramArguments</key>
179 <array>
180{program_args}
181 </array>
182 <key>RunAtLoad</key>
183 <true/>
184 <key>KeepAlive</key>
185 <true/>
186 <key>StandardOutPath</key>
187 <string>{stdout}</string>
188 <key>StandardErrorPath</key>
189 <string>{stderr}</string>
190</dict>
191</plist>"#,
192 stdout = log_dir.join("proxy.stdout.log").display(),
193 stderr = log_dir.join("proxy.stderr.log").display(),
194 );
195
196 let _ = std::fs::write(&plist_path, &plist);
197
198 let ok = crate::core::launchd::bootstrap(PLIST_LABEL, &plist_path);
199
200 if !quiet {
201 if ok {
202 println!(" Installed LaunchAgent: {}", plist_path.display());
203 println!(" Proxy will start on login and restart if stopped");
204 } else {
205 println!(" Created LaunchAgent at {}", plist_path.display());
206 println!(" Load reported a problem; check: launchctl print {PLIST_LABEL}");
207 }
208 }
209}
210
211#[cfg(target_os = "macos")]
212fn uninstall_launchagent(quiet: bool) {
213 let plist_path = launchagent_path();
214 if !plist_path.exists() {
215 if !quiet {
216 println!(" LaunchAgent not installed, nothing to remove");
217 }
218 return;
219 }
220
221 crate::core::launchd::bootout(PLIST_LABEL, &plist_path);
222
223 let _ = std::fs::remove_file(&plist_path);
224 if !quiet {
225 println!(" Removed LaunchAgent: {}", plist_path.display());
226 }
227}
228
229#[cfg(target_os = "linux")]
230fn systemd_path() -> PathBuf {
231 dirs::home_dir()
232 .unwrap_or_else(|| PathBuf::from("/tmp"))
233 .join(".config/systemd/user")
234 .join(format!("{SYSTEMD_SERVICE}.service"))
235}
236
237#[cfg(target_os = "linux")]
238fn install_systemd(binary: &str, port: u16, quiet: bool) {
239 let service_dir = dirs::home_dir()
240 .unwrap_or_else(|| PathBuf::from("/tmp"))
241 .join(".config/systemd/user");
242 let _ = std::fs::create_dir_all(&service_dir);
243
244 let service_path = service_dir.join(format!("{SYSTEMD_SERVICE}.service"));
245
246 let unit = format!(
247 r"[Unit]
248Description=lean-ctx API Proxy
249After=network.target
250StartLimitIntervalSec=300
251StartLimitBurst=5
252
253[Service]
254Type=simple
255ExecStart={binary} proxy start --port={port}
256Restart=on-failure
257RestartSec=5
258StandardOutput=journal
259StandardError=journal
260Environment=RUST_LOG=info
261
262[Install]
263WantedBy=default.target
264"
265 );
266
267 let _ = std::fs::write(&service_path, &unit);
268
269 let _ = std::process::Command::new("systemctl")
270 .args(["--user", "daemon-reload"])
271 .output();
272
273 let result = std::process::Command::new("systemctl")
274 .args(["--user", "enable", "--now", SYSTEMD_SERVICE])
275 .output();
276
277 if !quiet {
278 match result {
279 Ok(o) if o.status.success() => {
280 println!(" Installed systemd user service: {SYSTEMD_SERVICE}");
281 println!(" Proxy will start on login and restart if stopped");
282 }
283 Ok(o) => {
284 let err = String::from_utf8_lossy(&o.stderr);
285 println!(" Created service file but enable failed: {err}");
286 }
287 Err(e) => {
288 println!(" Created service file at {}", service_path.display());
289 println!(" Could not enable: {e}");
290 }
291 }
292 }
293}
294
295#[cfg(target_os = "linux")]
296fn uninstall_systemd(quiet: bool) {
297 let service_path = systemd_path();
298 if !service_path.exists() {
299 if !quiet {
300 println!(" systemd service not installed, nothing to remove");
301 }
302 return;
303 }
304
305 let _ = std::process::Command::new("systemctl")
306 .args(["--user", "stop", SYSTEMD_SERVICE])
307 .output();
308 let _ = std::process::Command::new("systemctl")
309 .args(["--user", "disable", SYSTEMD_SERVICE])
310 .output();
311 let _ = std::fs::remove_file(&service_path);
312 let _ = std::process::Command::new("systemctl")
313 .args(["--user", "daemon-reload"])
314 .output();
315
316 if !quiet {
317 println!(" Removed systemd service: {SYSTEMD_SERVICE}");
318 }
319}
320
321pub fn find_binary() -> String {
322 crate::core::portable_binary::resolve_portable_binary()
323}