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