use crate::{AutoLaunch, LinuxLaunchMode, Result};
use std::{fs, io::Write, path::PathBuf};
impl AutoLaunch {
pub fn new(
app_name: &str,
app_path: &str,
launch_mode: LinuxLaunchMode,
args: &[impl AsRef<str>],
) -> AutoLaunch {
AutoLaunch {
app_name: app_name.into(),
app_path: app_path.into(),
launch_mode,
args: args.iter().map(|s| s.as_ref().to_string()).collect(),
}
}
pub fn enable(&self) -> Result<()> {
match self.launch_mode {
LinuxLaunchMode::XdgAutostart => self.enable_xdg_autostart(),
LinuxLaunchMode::Systemd => self.enable_systemd(),
}
}
fn enable_xdg_autostart(&self) -> Result<()> {
let data = build_xdg_autostart_data(&self.app_name, &self.app_path, &self.args);
let dir = get_xdg_autostart_dir()?;
if !dir.exists() {
fs::create_dir_all(&dir).or_else(|e| {
if e.kind() == std::io::ErrorKind::AlreadyExists {
Ok(())
} else {
Err(e)
}
})?;
}
let file_path = self.get_xdg_desktop_file()?;
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(file_path)?;
file.write_all(data.as_bytes())?;
Ok(())
}
fn enable_systemd(&self) -> Result<()> {
let data = build_systemd_service_data(&self.app_name, &self.app_path, &self.args);
let dir = get_systemd_user_dir()?;
if !dir.exists() {
fs::create_dir_all(&dir).or_else(|e| {
if e.kind() == std::io::ErrorKind::AlreadyExists {
Ok(())
} else {
Err(e)
}
})?;
}
let service_file = self.get_systemd_service_file()?;
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&service_file)?;
file.write_all(data.as_bytes())?;
self.systemctl_enable()?;
Ok(())
}
fn systemctl_enable(&self) -> Result<()> {
let service_name = format!("{}.service", self.app_name);
let output = std::process::Command::new("systemctl")
.args(&["--user", "enable", &service_name])
.output()?;
if !output.status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Failed to enable systemd service: {}",
String::from_utf8_lossy(&output.stderr)
),
)
.into());
}
Ok(())
}
pub fn disable(&self) -> Result<()> {
match self.launch_mode {
LinuxLaunchMode::XdgAutostart => self.disable_xdg_autostart(),
LinuxLaunchMode::Systemd => self.disable_systemd(),
}
}
fn disable_xdg_autostart(&self) -> Result<()> {
let file = self.get_xdg_desktop_file()?;
if file.exists() {
fs::remove_file(file)?;
}
Ok(())
}
fn disable_systemd(&self) -> Result<()> {
self.systemctl_disable()?;
let service_file = self.get_systemd_service_file()?;
if service_file.exists() {
fs::remove_file(service_file)?;
}
let _ = std::process::Command::new("systemctl")
.args(&["--user", "daemon-reload"])
.output();
Ok(())
}
fn systemctl_disable(&self) -> Result<()> {
let service_name = format!("{}.service", self.app_name);
let output = std::process::Command::new("systemctl")
.args(&["--user", "disable", &service_name])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.contains("No such file or directory") && !stderr.contains("not loaded") {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to disable systemd service: {}", stderr),
)
.into());
}
}
Ok(())
}
pub fn is_enabled(&self) -> Result<bool> {
match self.launch_mode {
LinuxLaunchMode::XdgAutostart => Ok(self.get_xdg_desktop_file()?.exists()),
LinuxLaunchMode::Systemd => self.is_systemd_enabled(),
}
}
fn is_systemd_enabled(&self) -> Result<bool> {
let service_name = format!("{}.service", self.app_name);
let output = std::process::Command::new("systemctl")
.args(&["--user", "is-enabled", &service_name])
.output()?;
Ok(output.status.success())
}
fn get_xdg_desktop_file(&self) -> Result<PathBuf> {
Ok(get_xdg_autostart_dir()?.join(format!("{}.desktop", self.app_name)))
}
fn get_systemd_service_file(&self) -> Result<PathBuf> {
Ok(get_systemd_user_dir()?.join(format!("{}.service", self.app_name)))
}
}
fn build_xdg_autostart_data(app_name: &str, app_path: &str, args: &[String]) -> String {
format!(
"[Desktop Entry]\n\
Type=Application\n\
Version=1.0\n\
Name={}\n\
Comment={} startup script\n\
Exec={} {}\n\
StartupNotify=false\n\
Terminal=false",
app_name,
app_name,
app_path,
args.join(" ")
)
}
fn build_systemd_service_data(app_name: &str, app_path: &str, args: &[String]) -> String {
let args_str = if args.is_empty() {
String::new()
} else {
format!(" {}", args.join(" "))
};
format!(
"[Unit]\n\
Description={}\n\
After=default.target\n\
\n\
[Service]\n\
Type=simple\n\
ExecStart={}{}\n\
Restart=on-failure\n\
RestartSec=10\n\
\n\
[Install]\n\
WantedBy=default.target",
app_name, app_path, args_str
)
}
fn get_xdg_autostart_dir() -> Result<PathBuf> {
let home_dir = dirs::home_dir().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "Failed to find home directory")
})?;
Ok(home_dir.join(".config").join("autostart"))
}
fn get_systemd_user_dir() -> Result<PathBuf> {
let home_dir = dirs::home_dir().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "Failed to find home directory")
})?;
Ok(home_dir.join(".config").join("systemd").join("user"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_xdg_autostart_data() {
let data = build_xdg_autostart_data(
"TestApp",
"/opt/test-app",
&vec!["--flag".into(), "value".into()],
);
assert!(data.contains("Type=Application"));
assert!(data.contains("Name=TestApp"));
assert!(data.contains("Comment=TestApp startup script"));
assert!(data.contains("Exec=/opt/test-app --flag value"));
assert!(data.contains("StartupNotify=false"));
assert!(data.contains("Terminal=false"));
}
#[test]
fn test_build_systemd_service_data() {
let data = build_systemd_service_data(
"TestApp",
"/opt/test-app",
&vec!["--flag".into()],
);
assert!(data.contains("Description=TestApp"));
assert!(data.contains("After=default.target"));
assert!(data.contains("ExecStart=/opt/test-app --flag"));
assert!(data.contains("Restart=on-failure"));
assert!(data.contains("WantedBy=default.target"));
}
}