1use crate::{AutoLaunch, LinuxLaunchMode, Result};
2use std::{fs, io::Write, path::PathBuf};
3
4impl AutoLaunch {
6 pub fn new(
16 app_name: &str,
17 app_path: &str,
18 launch_mode: LinuxLaunchMode,
19 args: &[impl AsRef<str>],
20 ) -> AutoLaunch {
21 AutoLaunch {
22 app_name: app_name.into(),
23 app_path: app_path.into(),
24 launch_mode,
25 args: args.iter().map(|s| s.as_ref().to_string()).collect(),
26 }
27 }
28
29 pub fn enable(&self) -> Result<()> {
38 match self.launch_mode {
39 LinuxLaunchMode::XdgAutostart => self.enable_xdg_autostart(),
40 LinuxLaunchMode::SystemdUser | LinuxLaunchMode::SystemdSystem => self.enable_systemd(),
41 }
42 }
43
44 fn enable_xdg_autostart(&self) -> Result<()> {
46 let data = build_xdg_autostart_data(&self.app_name, &self.app_path, &self.args);
47
48 let dir = get_xdg_autostart_dir()?;
49 if !dir.exists() {
50 fs::create_dir_all(&dir).or_else(|e| {
51 if e.kind() == std::io::ErrorKind::AlreadyExists {
52 Ok(())
53 } else {
54 Err(e)
55 }
56 })?;
57 }
58 let file_path = self.get_xdg_desktop_file()?;
59 let mut file = fs::OpenOptions::new()
60 .write(true)
61 .create(true)
62 .truncate(true)
63 .open(file_path)?;
64 file.write_all(data.as_bytes())?;
65 Ok(())
66 }
67
68 fn enable_systemd(&self) -> Result<()> {
70 let data = build_systemd_service_data(
72 &self.app_name,
73 &self.app_path,
74 &self.args,
75 self.launch_mode,
76 );
77
78 let dir = get_systemd_dir(self.launch_mode)?;
80 if !dir.exists() {
81 fs::create_dir_all(&dir).or_else(|e| {
82 if e.kind() == std::io::ErrorKind::AlreadyExists {
83 Ok(())
84 } else {
85 Err(e)
86 }
87 })?;
88 }
89
90 let service_file = self.get_systemd_service_file()?;
92 let mut file = fs::OpenOptions::new()
93 .write(true)
94 .create(true)
95 .truncate(true)
96 .open(&service_file)?;
97 file.write_all(data.as_bytes())?;
98
99 let daemon_reload_args: &[&str] = match self.launch_mode {
101 LinuxLaunchMode::SystemdUser => &["--user", "daemon-reload"],
102 LinuxLaunchMode::SystemdSystem => &["daemon-reload"],
103 LinuxLaunchMode::XdgAutostart => unreachable!("XDG mode does not use systemctl"),
104 };
105 let _ = std::process::Command::new("systemctl")
106 .args(daemon_reload_args)
107 .output();
108
109 self.systemctl_enable()?;
111
112 Ok(())
113 }
114
115 fn systemctl_enable(&self) -> Result<()> {
117 let service_name = format!("{}.service", self.app_name);
118 let args: &[&str] = match self.launch_mode {
119 LinuxLaunchMode::SystemdUser => &["--user", "enable", &service_name],
120 LinuxLaunchMode::SystemdSystem => &["enable", &service_name],
121 LinuxLaunchMode::XdgAutostart => unreachable!("XDG mode does not use systemctl"),
122 };
123 let output = std::process::Command::new("systemctl")
124 .args(args)
125 .output()?;
126
127 if !output.status.success() {
128 return Err(std::io::Error::other(format!(
129 "Failed to enable systemd service: {}",
130 String::from_utf8_lossy(&output.stderr)
131 ))
132 .into());
133 }
134
135 Ok(())
136 }
137
138 pub fn disable(&self) -> Result<()> {
145 match self.launch_mode {
146 LinuxLaunchMode::XdgAutostart => self.disable_xdg_autostart(),
147 LinuxLaunchMode::SystemdUser | LinuxLaunchMode::SystemdSystem => self.disable_systemd(),
148 }
149 }
150
151 fn disable_xdg_autostart(&self) -> Result<()> {
153 let file = self.get_xdg_desktop_file()?;
154 if file.exists() {
155 fs::remove_file(file)?;
156 }
157 Ok(())
158 }
159
160 fn disable_systemd(&self) -> Result<()> {
162 self.systemctl_disable()?;
164
165 let service_file = self.get_systemd_service_file()?;
167 if service_file.exists() {
168 fs::remove_file(service_file)?;
169 }
170
171 let daemon_reload_args: &[&str] = match self.launch_mode {
173 LinuxLaunchMode::SystemdUser => &["--user", "daemon-reload"],
174 LinuxLaunchMode::SystemdSystem => &["daemon-reload"],
175 LinuxLaunchMode::XdgAutostart => unreachable!("XDG mode does not use systemctl"),
176 };
177 let _ = std::process::Command::new("systemctl")
178 .args(daemon_reload_args)
179 .output();
180
181 Ok(())
182 }
183
184 fn systemctl_disable(&self) -> Result<()> {
186 let service_name = format!("{}.service", self.app_name);
187 let args: &[&str] = match self.launch_mode {
188 LinuxLaunchMode::SystemdUser => &["--user", "disable", &service_name],
189 LinuxLaunchMode::SystemdSystem => &["disable", &service_name],
190 LinuxLaunchMode::XdgAutostart => unreachable!("XDG mode does not use systemctl"),
191 };
192 let output = std::process::Command::new("systemctl")
193 .args(args)
194 .output()?;
195
196 if !output.status.success() {
198 let stderr = String::from_utf8_lossy(&output.stderr);
199 if !stderr.contains("No such file or directory") && !stderr.contains("not loaded") {
200 let err_msg = format!("Failed to disable systemd service: {}", stderr);
201 return Err(std::io::Error::other(err_msg).into());
202 }
203 }
204
205 Ok(())
206 }
207
208 pub fn is_enabled(&self) -> Result<bool> {
210 match self.launch_mode {
211 LinuxLaunchMode::XdgAutostart => Ok(self.get_xdg_desktop_file()?.exists()),
212 LinuxLaunchMode::SystemdUser | LinuxLaunchMode::SystemdSystem => {
213 self.is_systemd_enabled()
214 }
215 }
216 }
217
218 fn is_systemd_enabled(&self) -> Result<bool> {
220 let service_name = format!("{}.service", self.app_name);
221 let args: &[&str] = match self.launch_mode {
222 LinuxLaunchMode::SystemdUser => &["--user", "is-enabled", &service_name],
223 LinuxLaunchMode::SystemdSystem => &["is-enabled", &service_name],
224 LinuxLaunchMode::XdgAutostart => unreachable!("XDG mode does not use systemctl"),
225 };
226 let output = std::process::Command::new("systemctl")
227 .args(args)
228 .output()?;
229
230 Ok(output.status.success())
235 }
236
237 pub fn get_registered_app_path(&self) -> Result<Option<String>> {
243 let file = match self.launch_mode {
244 LinuxLaunchMode::XdgAutostart => self.get_xdg_desktop_file()?,
245 LinuxLaunchMode::SystemdUser | LinuxLaunchMode::SystemdSystem => {
246 self.get_systemd_service_file()?
247 }
248 };
249 if !file.exists() {
250 return Ok(None);
251 }
252 let content = fs::read_to_string(file)?;
253 let key = match self.launch_mode {
254 LinuxLaunchMode::XdgAutostart => "Exec=",
255 _ => "ExecStart=",
256 };
257 let path = content
258 .lines()
259 .find_map(|line| {
260 let trimmed = line.trim();
261 trimmed.strip_prefix(key).map(|rest| {
262 rest.split_whitespace().next().map(|s| s.to_string())
265 })
266 })
267 .flatten();
268 Ok(path)
269 }
270
271 fn get_xdg_desktop_file(&self) -> Result<PathBuf> {
273 Ok(get_xdg_autostart_dir()?.join(format!("{}.desktop", self.app_name)))
274 }
275
276 fn get_systemd_service_file(&self) -> Result<PathBuf> {
278 Ok(get_systemd_dir(self.launch_mode)?.join(format!("{}.service", self.app_name)))
279 }
280}
281
282fn build_xdg_autostart_data(app_name: &str, app_path: &str, args: &[String]) -> String {
283 format!(
284 "[Desktop Entry]\n\
285 Type=Application\n\
286 Version=1.0\n\
287 Name={}\n\
288 Comment={} startup script\n\
289 Exec={} {}\n\
290 StartupNotify=false\n\
291 Terminal=false",
292 app_name,
293 app_name,
294 app_path,
295 args.join(" ")
296 )
297}
298
299fn build_systemd_service_data(
300 app_name: &str,
301 app_path: &str,
302 args: &[String],
303 mode: LinuxLaunchMode,
304) -> String {
305 let args_str = if args.is_empty() {
306 String::new()
307 } else {
308 format!(" {}", args.join(" "))
309 };
310
311 let wanted_by = match mode {
313 LinuxLaunchMode::SystemdSystem => "multi-user.target",
314 _ => "default.target",
315 };
316
317 format!(
318 "[Unit]\n\
319 Description={}\n\
320 After={}\n\
321 \n\
322 [Service]\n\
323 Type=simple\n\
324 ExecStart={}{}\n\
325 Restart=on-failure\n\
326 RestartSec=10\n\
327 \n\
328 [Install]\n\
329 WantedBy={}",
330 app_name, wanted_by, app_path, args_str, wanted_by
331 )
332}
333
334fn get_xdg_autostart_dir() -> Result<PathBuf> {
336 let home_dir = dirs::home_dir().ok_or_else(|| {
337 std::io::Error::new(
338 std::io::ErrorKind::NotFound,
339 "Failed to find home directory",
340 )
341 })?;
342 Ok(home_dir.join(".config").join("autostart"))
343}
344
345fn get_systemd_dir(mode: LinuxLaunchMode) -> Result<PathBuf> {
347 match mode {
348 LinuxLaunchMode::SystemdSystem => Ok(PathBuf::from("/etc/systemd/system")),
349 LinuxLaunchMode::SystemdUser => {
350 let home_dir = dirs::home_dir().ok_or_else(|| {
351 std::io::Error::new(
352 std::io::ErrorKind::NotFound,
353 "Failed to find home directory",
354 )
355 })?;
356 Ok(home_dir.join(".config").join("systemd").join("user"))
357 }
358 LinuxLaunchMode::XdgAutostart => unreachable!("XDG mode does not use systemd dir"),
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_build_xdg_autostart_data() {
368 let data = build_xdg_autostart_data(
369 "TestApp",
370 "/opt/test-app",
371 &["--flag".into(), "value".into()],
372 );
373
374 assert!(data.contains("Type=Application"));
375 assert!(data.contains("Name=TestApp"));
376 assert!(data.contains("Comment=TestApp startup script"));
377 assert!(data.contains("Exec=/opt/test-app --flag value"));
378 assert!(data.contains("StartupNotify=false"));
379 assert!(data.contains("Terminal=false"));
380 }
381
382 #[test]
383 fn test_build_systemd_service_data() {
384 let data = build_systemd_service_data(
385 "TestApp",
386 "/opt/test-app",
387 &["--flag".into()],
388 LinuxLaunchMode::SystemdUser,
389 );
390
391 assert!(data.contains("Description=TestApp"));
392 assert!(data.contains("After=default.target"));
393 assert!(data.contains("ExecStart=/opt/test-app --flag"));
394 assert!(data.contains("Restart=on-failure"));
395 assert!(data.contains("WantedBy=default.target"));
396 }
397
398 #[test]
399 fn test_build_systemd_service_data_system() {
400 let data = build_systemd_service_data(
401 "TestApp",
402 "/opt/test-app",
403 &["--flag".into()],
404 LinuxLaunchMode::SystemdSystem,
405 );
406
407 assert!(data.contains("After=multi-user.target"));
408 assert!(data.contains("WantedBy=multi-user.target"));
409 }
410}