auths_cli/commands/agent/
service.rs1use anyhow::{Context, Result, anyhow};
4use clap::ValueEnum;
5use std::fs;
6use std::io::IsTerminal;
7use std::path::{Path, PathBuf};
8
9use super::{get_default_socket_path, get_log_file_path};
10
11fn confirm_overwrite(path: &Path, force: bool) -> Result<()> {
26 if force {
27 return Ok(());
28 }
29 if !std::io::stdin().is_terminal() {
30 return Err(anyhow!(
31 "Service already installed at {}. Use --force to overwrite.",
32 path.display()
33 ));
34 }
35 let confirmed = dialoguer::Confirm::new()
36 .with_prompt(format!(
37 "A service is already installed at {}. Overwrite it? This replaces what runs as your agent.",
38 path.display()
39 ))
40 .default(false)
41 .interact()
42 .context("Failed to read overwrite confirmation")?;
43 if !confirmed {
44 return Err(anyhow!("Aborted: the existing service was left unchanged."));
45 }
46 Ok(())
47}
48
49#[derive(ValueEnum, Clone, Debug, PartialEq)]
51pub enum ServiceManager {
52 Launchd,
54 Systemd,
56}
57
58pub fn detect_service_manager() -> Option<ServiceManager> {
66 #[cfg(target_os = "macos")]
67 {
68 Some(ServiceManager::Launchd)
69 }
70 #[cfg(target_os = "linux")]
71 {
72 if std::path::Path::new("/run/systemd/system").exists() {
73 Some(ServiceManager::Systemd)
74 } else {
75 None
76 }
77 }
78 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
79 {
80 None
81 }
82}
83
84fn get_launchd_plist_path() -> Result<PathBuf> {
85 let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
86 Ok(home
87 .join("Library")
88 .join("LaunchAgents")
89 .join("com.auths.agent.plist"))
90}
91
92fn get_systemd_unit_path() -> Result<PathBuf> {
93 let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
94 Ok(home
95 .join(".config")
96 .join("systemd")
97 .join("user")
98 .join("auths-agent.service"))
99}
100
101fn generate_launchd_plist() -> Result<String> {
102 let exe_path = std::env::current_exe().context("Failed to get current executable path")?;
103 let exe_str = exe_path
104 .to_str()
105 .ok_or_else(|| anyhow!("Executable path is not valid UTF-8"))?;
106
107 let socket_path = get_default_socket_path()?;
108 let socket_str = socket_path
109 .to_str()
110 .ok_or_else(|| anyhow!("Socket path is not valid UTF-8"))?;
111
112 let log_path = get_log_file_path()?;
113 let log_str = log_path
114 .to_str()
115 .ok_or_else(|| anyhow!("Log path is not valid UTF-8"))?;
116
117 Ok(format!(
118 r#"<?xml version="1.0" encoding="UTF-8"?>
119<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
120<plist version="1.0">
121<dict>
122 <key>Label</key>
123 <string>com.auths.agent</string>
124 <key>ProgramArguments</key>
125 <array>
126 <string>{exe}</string>
127 <string>agent</string>
128 <string>start</string>
129 <string>--foreground</string>
130 </array>
131 <key>RunAtLoad</key>
132 <true/>
133 <key>KeepAlive</key>
134 <true/>
135 <key>StandardOutPath</key>
136 <string>{log}</string>
137 <key>StandardErrorPath</key>
138 <string>{log}</string>
139 <key>EnvironmentVariables</key>
140 <dict>
141 <key>SSH_AUTH_SOCK</key>
142 <string>{socket}</string>
143 </dict>
144</dict>
145</plist>
146"#,
147 exe = exe_str,
148 log = log_str,
149 socket = socket_str
150 ))
151}
152
153fn generate_systemd_unit() -> Result<String> {
154 let exe_path = std::env::current_exe().context("Failed to get current executable path")?;
155 let exe_str = exe_path
156 .to_str()
157 .ok_or_else(|| anyhow!("Executable path is not valid UTF-8"))?;
158
159 Ok(format!(
160 r#"[Unit]
161Description=Auths SSH Agent
162Documentation=https://github.com/auths-rs/auths
163
164[Service]
165Type=simple
166ExecStart={exe} agent start --foreground
167Restart=on-failure
168RestartSec=5
169
170[Install]
171WantedBy=default.target
172"#,
173 exe = exe_str
174 ))
175}
176
177pub fn install_service(dry_run: bool, force: bool, manager: Option<ServiceManager>) -> Result<()> {
189 let manager = manager
190 .or_else(detect_service_manager)
191 .ok_or_else(|| anyhow!("No supported service manager found on this platform"))?;
192
193 match manager {
194 ServiceManager::Launchd => install_launchd_service(dry_run, force),
195 ServiceManager::Systemd => install_systemd_service(dry_run, force),
196 }
197}
198
199fn install_launchd_service(dry_run: bool, force: bool) -> Result<()> {
200 let plist_content = generate_launchd_plist()?;
201 let plist_path = get_launchd_plist_path()?;
202
203 if dry_run {
204 eprintln!("Would install to: {}", plist_path.display());
205 eprintln!();
206 println!("{}", plist_content);
207 return Ok(());
208 }
209
210 if plist_path.exists() {
211 confirm_overwrite(&plist_path, force)?;
212 }
213
214 if let Some(parent) = plist_path.parent() {
215 fs::create_dir_all(parent)
216 .with_context(|| format!("Failed to create directory: {:?}", parent))?;
217 }
218
219 fs::write(&plist_path, &plist_content)
220 .with_context(|| format!("Failed to write plist: {:?}", plist_path))?;
221
222 eprintln!("Installed launchd service: {}", plist_path.display());
223 eprintln!();
224 eprintln!("To start the service now:");
225 eprintln!(" launchctl load {}", plist_path.display());
226 eprintln!();
227 eprintln!("The agent will start automatically on login.");
228
229 Ok(())
230}
231
232fn install_systemd_service(dry_run: bool, force: bool) -> Result<()> {
233 let unit_content = generate_systemd_unit()?;
234 let unit_path = get_systemd_unit_path()?;
235
236 if dry_run {
237 eprintln!("Would install to: {}", unit_path.display());
238 eprintln!();
239 println!("{}", unit_content);
240 return Ok(());
241 }
242
243 if unit_path.exists() {
244 confirm_overwrite(&unit_path, force)?;
245 }
246
247 if let Some(parent) = unit_path.parent() {
248 fs::create_dir_all(parent)
249 .with_context(|| format!("Failed to create directory: {:?}", parent))?;
250 }
251
252 fs::write(&unit_path, &unit_content)
253 .with_context(|| format!("Failed to write unit file: {:?}", unit_path))?;
254
255 eprintln!("Installed systemd service: {}", unit_path.display());
256 eprintln!();
257 eprintln!("To enable and start the service:");
258 eprintln!(" systemctl --user daemon-reload");
259 eprintln!(" systemctl --user enable --now auths-agent");
260 eprintln!();
261 eprintln!("The agent will start automatically on login.");
262
263 Ok(())
264}
265
266pub fn uninstall_service() -> Result<()> {
273 let manager = detect_service_manager()
274 .ok_or_else(|| anyhow!("No supported service manager found on this platform"))?;
275
276 match manager {
277 ServiceManager::Launchd => uninstall_launchd_service(),
278 ServiceManager::Systemd => uninstall_systemd_service(),
279 }
280}
281
282fn uninstall_launchd_service() -> Result<()> {
283 let plist_path = get_launchd_plist_path()?;
284
285 if !plist_path.exists() {
286 return Err(anyhow!("Service not installed at {}", plist_path.display()));
287 }
288
289 eprintln!("Unloading launchd service...");
290 let _ = std::process::Command::new("launchctl")
291 .arg("unload")
292 .arg(&plist_path)
293 .status();
294
295 fs::remove_file(&plist_path)
296 .with_context(|| format!("Failed to remove plist: {:?}", plist_path))?;
297
298 eprintln!("Uninstalled launchd service: {}", plist_path.display());
299 Ok(())
300}
301
302fn uninstall_systemd_service() -> Result<()> {
303 let unit_path = get_systemd_unit_path()?;
304
305 if !unit_path.exists() {
306 return Err(anyhow!("Service not installed at {}", unit_path.display()));
307 }
308
309 eprintln!("Stopping and disabling systemd service...");
310 let _ = std::process::Command::new("systemctl")
311 .args(["--user", "disable", "--now", "auths-agent"])
312 .status();
313
314 fs::remove_file(&unit_path)
315 .with_context(|| format!("Failed to remove unit file: {:?}", unit_path))?;
316
317 let _ = std::process::Command::new("systemctl")
318 .args(["--user", "daemon-reload"])
319 .status();
320
321 eprintln!("Uninstalled systemd service: {}", unit_path.display());
322 Ok(())
323}