auto_launch/
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::Systemd => 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 user service
69    fn enable_systemd(&self) -> Result<()> {
70        // Create systemd service file content
71        let data = build_systemd_service_data(&self.app_name, &self.app_path, &self.args);
72
73        // Create systemd user directory
74        let dir = get_systemd_user_dir()?;
75        if !dir.exists() {
76            fs::create_dir_all(&dir).or_else(|e| {
77                if e.kind() == std::io::ErrorKind::AlreadyExists {
78                    Ok(())
79                } else {
80                    Err(e)
81                }
82            })?;
83        }
84
85        // Write service file
86        let service_file = self.get_systemd_service_file()?;
87        let mut file = fs::OpenOptions::new()
88            .write(true)
89            .create(true)
90            .truncate(true)
91            .open(&service_file)?;
92        file.write_all(data.as_bytes())?;
93
94        // Enable and start the service using systemctl
95        self.systemctl_enable()?;
96
97        Ok(())
98    }
99
100    /// Run systemctl --user enable command
101    fn systemctl_enable(&self) -> Result<()> {
102        let service_name = format!("{}.service", self.app_name);
103        let output = std::process::Command::new("systemctl")
104            .args(&["--user", "enable", &service_name])
105            .output()?;
106
107        if !output.status.success() {
108            return Err(std::io::Error::new(
109                std::io::ErrorKind::Other,
110                format!(
111                    "Failed to enable systemd service: {}",
112                    String::from_utf8_lossy(&output.stderr)
113                ),
114            )
115            .into());
116        }
117
118        Ok(())
119    }
120
121    /// Disable the AutoLaunch setting
122    ///
123    /// ## Errors
124    ///
125    /// - failed to remove file
126    /// - failed to disable systemd service (if using systemd mode)
127    pub fn disable(&self) -> Result<()> {
128        match self.launch_mode {
129            LinuxLaunchMode::XdgAutostart => self.disable_xdg_autostart(),
130            LinuxLaunchMode::Systemd => self.disable_systemd(),
131        }
132    }
133
134    /// Disable XDG Autostart
135    fn disable_xdg_autostart(&self) -> Result<()> {
136        let file = self.get_xdg_desktop_file()?;
137        if file.exists() {
138            fs::remove_file(file)?;
139        }
140        Ok(())
141    }
142
143    /// Disable systemd user service
144    fn disable_systemd(&self) -> Result<()> {
145        // Disable the service
146        self.systemctl_disable()?;
147
148        // Remove service file
149        let service_file = self.get_systemd_service_file()?;
150        if service_file.exists() {
151            fs::remove_file(service_file)?;
152        }
153
154        // Reload systemd daemon
155        let _ = std::process::Command::new("systemctl")
156            .args(&["--user", "daemon-reload"])
157            .output();
158
159        Ok(())
160    }
161
162    /// Run systemctl --user disable command
163    fn systemctl_disable(&self) -> Result<()> {
164        let service_name = format!("{}.service", self.app_name);
165        let output = std::process::Command::new("systemctl")
166            .args(&["--user", "disable", &service_name])
167            .output()?;
168
169        // Don't fail if the service is not enabled
170        if !output.status.success() {
171            let stderr = String::from_utf8_lossy(&output.stderr);
172            if !stderr.contains("No such file or directory") && !stderr.contains("not loaded") {
173                return Err(std::io::Error::new(
174                    std::io::ErrorKind::Other,
175                    format!("Failed to disable systemd service: {}", stderr),
176                )
177                .into());
178            }
179        }
180
181        Ok(())
182    }
183
184    /// Check whether the AutoLaunch setting is enabled
185    pub fn is_enabled(&self) -> Result<bool> {
186        match self.launch_mode {
187            LinuxLaunchMode::XdgAutostart => Ok(self.get_xdg_desktop_file()?.exists()),
188            LinuxLaunchMode::Systemd => self.is_systemd_enabled(),
189        }
190    }
191
192    /// Check if systemd service is enabled
193    fn is_systemd_enabled(&self) -> Result<bool> {
194        let service_name = format!("{}.service", self.app_name);
195        let output = std::process::Command::new("systemctl")
196            .args(&["--user", "is-enabled", &service_name])
197            .output()?;
198
199        // systemctl is-enabled returns:
200        // - "enabled" with exit code 0 if enabled
201        // - "disabled" with exit code 1 if disabled
202        // - other states or errors with other exit codes
203        Ok(output.status.success())
204    }
205
206    /// Get the XDG desktop entry file path
207    fn get_xdg_desktop_file(&self) -> Result<PathBuf> {
208        Ok(get_xdg_autostart_dir()?.join(format!("{}.desktop", self.app_name)))
209    }
210
211    /// Get the systemd service file path
212    fn get_systemd_service_file(&self) -> Result<PathBuf> {
213        Ok(get_systemd_user_dir()?.join(format!("{}.service", self.app_name)))
214    }
215}
216
217fn build_xdg_autostart_data(app_name: &str, app_path: &str, args: &[String]) -> String {
218    format!(
219        "[Desktop Entry]\n\
220        Type=Application\n\
221        Version=1.0\n\
222        Name={}\n\
223        Comment={} startup script\n\
224        Exec={} {}\n\
225        StartupNotify=false\n\
226        Terminal=false",
227        app_name,
228        app_name,
229        app_path,
230        args.join(" ")
231    )
232}
233
234fn build_systemd_service_data(app_name: &str, app_path: &str, args: &[String]) -> String {
235    let args_str = if args.is_empty() {
236        String::new()
237    } else {
238        format!(" {}", args.join(" "))
239    };
240
241    format!(
242        "[Unit]\n\
243        Description={}\n\
244        After=default.target\n\
245        \n\
246        [Service]\n\
247        Type=simple\n\
248        ExecStart={}{}\n\
249        Restart=on-failure\n\
250        RestartSec=10\n\
251        \n\
252        [Install]\n\
253        WantedBy=default.target",
254        app_name, app_path, args_str
255    )
256}
257
258/// Get the XDG autostart directory
259fn get_xdg_autostart_dir() -> Result<PathBuf> {
260    let home_dir = dirs::home_dir().ok_or_else(|| {
261        std::io::Error::new(std::io::ErrorKind::NotFound, "Failed to find home directory")
262    })?;
263    Ok(home_dir.join(".config").join("autostart"))
264}
265
266/// Get the systemd user service directory
267fn get_systemd_user_dir() -> Result<PathBuf> {
268    let home_dir = dirs::home_dir().ok_or_else(|| {
269        std::io::Error::new(std::io::ErrorKind::NotFound, "Failed to find home directory")
270    })?;
271    Ok(home_dir.join(".config").join("systemd").join("user"))
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_build_xdg_autostart_data() {
280        let data = build_xdg_autostart_data(
281            "TestApp",
282            "/opt/test-app",
283            &vec!["--flag".into(), "value".into()],
284        );
285
286        assert!(data.contains("Type=Application"));
287        assert!(data.contains("Name=TestApp"));
288        assert!(data.contains("Comment=TestApp startup script"));
289        assert!(data.contains("Exec=/opt/test-app --flag value"));
290        assert!(data.contains("StartupNotify=false"));
291        assert!(data.contains("Terminal=false"));
292    }
293
294    #[test]
295    fn test_build_systemd_service_data() {
296        let data = build_systemd_service_data(
297            "TestApp",
298            "/opt/test-app",
299            &vec!["--flag".into()],
300        );
301
302        assert!(data.contains("Description=TestApp"));
303        assert!(data.contains("After=default.target"));
304        assert!(data.contains("ExecStart=/opt/test-app --flag"));
305        assert!(data.contains("Restart=on-failure"));
306        assert!(data.contains("WantedBy=default.target"));
307    }
308}