Skip to main content

auto_launcher/
linux.rs

1use crate::{AutoLaunch, LinuxLaunchMode, Result};
2use std::{fs, io::Write, path::PathBuf};
3
4/// Linux implement
5impl AutoLaunch {
6    /// Create a new AutoLaunch instance
7    /// - `app_name`: application name
8    /// - `app_path`: application path
9    /// - `launch_mode`: launch mode (XDG Autostart or systemd)
10    /// - `args`: startup args passed to the binary
11    ///
12    /// ## Notes
13    ///
14    /// The parameters of `AutoLaunch::new` are different on each platform.
15    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    /// Enable the AutoLaunch setting
30    ///
31    /// ## Errors
32    ///
33    /// - failed to create directory
34    /// - failed to create file
35    /// - failed to write bytes to the file
36    /// - failed to enable systemd service (if using systemd mode)
37    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    /// Enable using XDG Autostart (.desktop file)
45    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    /// Enable using systemd service
69    fn enable_systemd(&self) -> Result<()> {
70        // Create systemd service file content
71        let data = build_systemd_service_data(
72            &self.app_name,
73            &self.app_path,
74            &self.args,
75            self.launch_mode,
76        );
77
78        // Create systemd directory
79        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        // Write service file
91        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        // Reload systemd daemon so it picks up the new service file
100        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        // Enable and start the service using systemctl
110        self.systemctl_enable()?;
111
112        Ok(())
113    }
114
115    /// Run systemctl enable command.
116    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    /// Disable the AutoLaunch setting
139    ///
140    /// ## Errors
141    ///
142    /// - failed to remove file
143    /// - failed to disable systemd service (if using systemd mode)
144    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    /// Disable XDG Autostart
152    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    /// Disable systemd service
161    fn disable_systemd(&self) -> Result<()> {
162        // Disable the service
163        self.systemctl_disable()?;
164
165        // Remove service file
166        let service_file = self.get_systemd_service_file()?;
167        if service_file.exists() {
168            fs::remove_file(service_file)?;
169        }
170
171        // Reload systemd daemon
172        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    /// Run systemctl disable command.
185    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        // Don't fail if the service is not enabled
197        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    /// Check whether the AutoLaunch setting is enabled
209    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    /// Check if systemd service is enabled
219    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        // systemctl is-enabled returns:
231        // - "enabled" with exit code 0 if enabled
232        // - "disabled" with exit code 1 if disabled
233        // - other states or errors with other exit codes
234        Ok(output.status.success())
235    }
236
237    /// Read the registered `app_path` from the on-disk service/desktop file.
238    ///
239    /// Returns `Ok(None)` when the registration does not exist.
240    /// For systemd, extracts the binary from the `ExecStart=` line.
241    /// For XDG autostart, extracts it from the `Exec=` line.
242    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                    // ExecStart=/path/to/bin arg1 arg2
263                    // Take the first whitespace-delimited token.
264                    rest.split_whitespace().next().map(|s| s.to_string())
265                })
266            })
267            .flatten();
268        Ok(path)
269    }
270
271    /// Get the XDG desktop entry file path
272    fn get_xdg_desktop_file(&self) -> Result<PathBuf> {
273        Ok(get_xdg_autostart_dir()?.join(format!("{}.desktop", self.app_name)))
274    }
275
276    /// Get the systemd service file path
277    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    // system services should target multi-user.target; user services use default.target
312    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
334/// Get the XDG autostart directory
335fn 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
345/// Get the systemd service directory.
346fn 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}